Skip to content

Commit 66c6532

Browse files
authored
Allow users with workspace write permission to edit notebook (#258)
Signed-off-by: Owen Wang <[email protected]>
1 parent 7d43f93 commit 66c6532

File tree

10 files changed

+80
-70
lines changed

10 files changed

+80
-70
lines changed

common/state/notebook_state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface NotebookStateValue {
2525
runningMemory?: AgenticMemeory;
2626
historyMemory?: AgenticMemeory;
2727
investigationError?: string;
28-
isNotebookOwner: boolean;
28+
isNotebookReadonly: boolean;
2929
}
3030

3131
export class NotebookState extends ObservableState<NotebookStateValue> {

public/components/notebooks/components/agentic_notebook.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,16 @@ interface AgenticNotebookProps extends NotebookComponentProps {
5959

6060
function NotebookComponent({ showPageHeader }: NotebookComponentProps) {
6161
const {
62-
services: { notifications, findingService, chrome, chat, uiSettings, contextProvider },
62+
services: {
63+
notifications,
64+
findingService,
65+
chrome,
66+
chat,
67+
uiSettings,
68+
contextProvider,
69+
workspaces,
70+
},
6371
} = useOpenSearchDashboards<NoteBookServices>();
64-
6572
const [isModalVisible, setIsModalVisible] = useState(false);
6673
const [isReinvestigateModalVisible, setIsReinvestigateModalVisible] = useState(false);
6774
const [modalLayout, setModalLayout] = useState<React.ReactNode>(<EuiOverlayMask />);
@@ -81,7 +88,7 @@ function NotebookComponent({ showPageHeader }: NotebookComponentProps) {
8188
id: openedNoteId,
8289
paragraphs: paragraphsStates,
8390
isLoading,
84-
isNotebookOwner,
91+
isNotebookReadonly,
8592
} = useObservable(notebookContext.state.getValue$(), notebookContext.state.value);
8693
const paraDivRefs = useRef<Array<HTMLDivElement | null>>([]);
8794

@@ -107,6 +114,13 @@ function NotebookComponent({ showPageHeader }: NotebookComponentProps) {
107114
closeModal();
108115
};
109116

117+
useEffect(() => {
118+
const subscription = workspaces.currentWorkspace$.subscribe((currentWorkspace) => {
119+
notebookContext.state.updateValue({ isNotebookReadonly: currentWorkspace?.readonly });
120+
});
121+
return () => subscription.unsubscribe();
122+
}, [workspaces.currentWorkspace$, notebookContext.state]);
123+
110124
// Initialize finding integration for automatic UI updates when findings are added
111125
useNotebookFindingIntegration({
112126
findingService,
@@ -159,7 +173,6 @@ function NotebookComponent({ showPageHeader }: NotebookComponentProps) {
159173
hypotheses: res.hypotheses,
160174
runningMemory: res.runningMemory,
161175
historyMemory: res.historyMemory,
162-
isNotebookOwner: res.isNotebookOwner,
163176
});
164177

165178
// Check if there's an ongoing investigation to continue BEFORE calling start
@@ -204,7 +217,7 @@ function NotebookComponent({ showPageHeader }: NotebookComponentProps) {
204217
// TODO: remove the optional chain after each method
205218
(chrome as any).setIsNavDrawerLocked?.(false);
206219
const rafId = window.requestAnimationFrame(() => {
207-
chat?.openWindow?.();
220+
(chat as any)?.openWindow?.();
208221
});
209222
return () => {
210223
window.cancelAnimationFrame(rafId);
@@ -346,7 +359,7 @@ function NotebookComponent({ showPageHeader }: NotebookComponentProps) {
346359
})
347360
: null}
348361

349-
{!isLoading && !isInvestigating && isNotebookOwner && (
362+
{!isLoading && !isInvestigating && !isNotebookReadonly && (
350363
<>
351364
<EuiSpacer size="s" />
352365
<EuiFlexGroup alignItems="center" gutterSize="none">

public/components/notebooks/components/hypothesis/hypotheses_panel.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const HypothesesPanel: React.FC<HypothesesPanelProps> = ({
5656
runningMemory,
5757
historyMemory,
5858
investigationError,
59-
isNotebookOwner,
59+
isNotebookReadonly,
6060
} = useObservable(notebookContext.state.getValue$(), notebookContext.state.value);
6161
const history = useHistory();
6262
const [showSteps, setShowSteps] = useState(false);
@@ -66,7 +66,7 @@ export const HypothesesPanel: React.FC<HypothesesPanelProps> = ({
6666
}, [isInvestigating, runningMemory, historyMemory]);
6767

6868
const PERAgentServices = useMemo(() => {
69-
if (!activeMemory?.executorMemoryId || !activeMemory?.memoryContainerId || !isNotebookOwner) {
69+
if (!activeMemory?.executorMemoryId || !activeMemory?.memoryContainerId || isNotebookReadonly) {
7070
return null;
7171
}
7272

@@ -80,7 +80,7 @@ export const HypothesesPanel: React.FC<HypothesesPanelProps> = ({
8080
if (!isInvestigating && activeMemory) {
8181
return false;
8282
}
83-
return !messageService.getMessageValue()?.hits?.hits?.[0]?._source?.structured_data
83+
return !(messageService.getMessageValue() as any)?.hits?.hits?.[0]?._source?.structured_data
8484
?.response;
8585
},
8686
activeMemory.memoryContainerId
@@ -90,7 +90,7 @@ export const HypothesesPanel: React.FC<HypothesesPanelProps> = ({
9090
message: messageService,
9191
executorMemory: executorMemoryService,
9292
};
93-
}, [http, activeMemory, isInvestigating, isNotebookOwner]);
93+
}, [http, activeMemory, isInvestigating, isNotebookReadonly]);
9494

9595
const executorMessages$ = useMemo(
9696
() => PERAgentServices?.executorMemory.getMessages$() ?? new BehaviorSubject<any[]>([]),
@@ -173,7 +173,7 @@ export const HypothesesPanel: React.FC<HypothesesPanelProps> = ({
173173
return null;
174174
}
175175

176-
const investigationSteps = PERAgentServices && isNotebookOwner && (
176+
const investigationSteps = PERAgentServices && !isNotebookReadonly && (
177177
<EuiAccordion
178178
id="investigation-steps"
179179
buttonContent="Investigation Steps"
@@ -257,7 +257,7 @@ export const HypothesesPanel: React.FC<HypothesesPanelProps> = ({
257257
AI Agent continuously evaluates and ranks hypotheses based on evidence
258258
</EuiText>
259259
</EuiFlexGroup>
260-
{hypotheses?.length && !isInvestigating ? (
260+
{hypotheses?.length && !isInvestigating && !isNotebookReadonly ? (
261261
<HypothesesFeedback
262262
appName={appName}
263263
usageCollection={usageCollection}
@@ -276,7 +276,7 @@ export const HypothesesPanel: React.FC<HypothesesPanelProps> = ({
276276
}}
277277
dataSourceId={context.value.dataSourceId}
278278
currentExecutorMemoryId={activeMemory?.executorMemoryId}
279-
memoryContainerId={activeMemory?.memoryContainerId}
279+
memoryContainerId={activeMemory?.memoryContainerId as string}
280280
/>
281281
)}
282282
</>

public/components/notebooks/components/paragraph_components/paragraph.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const Paragraph = (props: ParagraphProps) => {
4242
const isFindingParagraph =
4343
notebookType !== NotebookType.CLASSIC && paragraph.value.input.inputType === 'MARKDOWN';
4444
let isActionVisible = isClassicNotebook;
45-
if (!isClassicNotebook && isFindingParagraph && context.state.value.isNotebookOwner) {
45+
if (!isClassicNotebook && isFindingParagraph && !context.state.value.isNotebookReadonly) {
4646
isActionVisible = true;
4747
}
4848

public/components/notebooks/components/summary_card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const SummaryCard: React.FC<SummaryCardProps> = ({
4646
});
4747
};
4848

49-
const { isNotebookOwner } = useObservable(
49+
const { isNotebookReadonly } = useObservable(
5050
notebookContext.state.getValue$(),
5151
notebookContext.state.value
5252
);
@@ -93,7 +93,7 @@ export const SummaryCard: React.FC<SummaryCardProps> = ({
9393
<EuiTitle>
9494
<h2 style={{ paddingLeft: '20px' }}>Issue summary and impact</h2>
9595
</EuiTitle>
96-
{isNotebookOwner ? (
96+
{!isNotebookReadonly ? (
9797
<EuiButton
9898
style={{ marginRight: '20px' }}
9999
onClick={() => openReinvestigateModal()}

public/hooks/use_investigation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,10 @@ ${finding.evidence}
319319
context.state.updateValue({ investigationError: undefined });
320320

321321
try {
322-
if (!context.state.value.isNotebookOwner) {
323-
throw new Error('Only owner of this notebook can start the investigation');
322+
if (context.state.value.isNotebookReadonly) {
323+
throw new Error(
324+
'Only user with write permission of this notebook can start the investigation'
325+
);
324326
}
325327

326328
if (context.state.value.context.value.initialGoal !== question) {

public/hooks/use_notebook.tsx

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -88,53 +88,51 @@ export const useNotebook = () => {
8888
isLoading: true,
8989
});
9090

91-
const promise = http
92-
.get<NotebookBackendType & { isNotebookOwner: boolean }>(route)
93-
.then(async (res) => {
94-
let contextPayload = {
95-
...res.context,
96-
};
97-
98-
// try to convert relative time in ppl query to absolute time as
99-
if (res.context?.variables?.pplQuery) {
100-
const pplWithAbsoluteTime = parsePPLQuery(
101-
res.context.variables.pplQuery,
102-
res.context.currentTime
103-
).pplWithAbsoluteTime;
104-
if (pplWithAbsoluteTime !== res.context.variables.pplQuery) {
105-
contextPayload = {
106-
...contextPayload,
107-
variables: {
108-
...contextPayload.variables,
109-
pplQuery: pplWithAbsoluteTime,
110-
},
111-
};
112-
}
113-
}
114-
115-
if (
116-
!res.context?.indexInsight &&
117-
res.context?.index &&
118-
res.context?.timeField &&
119-
res.context?.timeRange
120-
) {
121-
let indexInsight;
122-
try {
123-
indexInsight = await fetchIndexInsights(res.context.index, res.context?.dataSourceId);
124-
} catch (error) {
125-
console.error('Failed to load index insight:', error);
126-
}
91+
const promise = http.get<NotebookBackendType>(route).then(async (res) => {
92+
let contextPayload = {
93+
...res.context,
94+
};
95+
96+
// try to convert relative time in ppl query to absolute time as
97+
if (res.context?.variables?.pplQuery) {
98+
const pplWithAbsoluteTime = parsePPLQuery(
99+
res.context.variables.pplQuery,
100+
res.context.currentTime
101+
).pplWithAbsoluteTime;
102+
if (pplWithAbsoluteTime !== res.context.variables.pplQuery) {
127103
contextPayload = {
128104
...contextPayload,
129-
indexInsight,
105+
variables: {
106+
...contextPayload.variables,
107+
pplQuery: pplWithAbsoluteTime,
108+
},
130109
};
131110
}
132-
return {
133-
...res,
134-
vizPrefix: res.vizPrefix || '',
135-
context: contextPayload,
111+
}
112+
113+
if (
114+
!res.context?.indexInsight &&
115+
res.context?.index &&
116+
res.context?.timeField &&
117+
res.context?.timeRange
118+
) {
119+
let indexInsight;
120+
try {
121+
indexInsight = await fetchIndexInsights(res.context.index, res.context?.dataSourceId);
122+
} catch (error) {
123+
console.error('Failed to load index insight:', error);
124+
}
125+
contextPayload = {
126+
...contextPayload,
127+
indexInsight,
136128
};
137-
});
129+
}
130+
return {
131+
...res,
132+
vizPrefix: res.vizPrefix || '',
133+
context: contextPayload,
134+
};
135+
});
138136

139137
promise.finally(() => {
140138
context.state.updateValue({

public/plugin.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export class InvestigationPlugin
107107
findingService,
108108
usageCollection: depsStart.usageCollection,
109109
overlay: depsStart.overlay,
110+
workspaces: coreStart.workspaces,
110111
};
111112
return services;
112113
};

server/adaptors/notebooks/saved_objects_notebooks_router.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export function cloneNotebook(fetchedNotebook: NotebookBackendType, name: string
6969
backend: 'kibana_1.0',
7070
paragraphs: fetchedNotebook.paragraphs,
7171
path: name,
72+
owner: fetchedNotebook.owner,
7273
};
7374

7475
return {

server/routes/notebooks/notebook_router.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export function registerNoteRoute(router: IRouter, auth: HttpAuth) {
219219
request.body.notebookId,
220220
opensearchNotebooksClient
221221
);
222-
noteBookInfo.attributes.savedNotebook = {
222+
(noteBookInfo.attributes as any).savedNotebook = {
223223
...noteBookInfo.attributes.savedNotebook,
224224
...noteObject,
225225
};
@@ -265,14 +265,9 @@ export function registerNoteRoute(router: IRouter, auth: HttpAuth) {
265265
NOTEBOOK_SAVED_OBJECT,
266266
request.params.noteId
267267
);
268-
const savedNotebook = notebookinfo.attributes.savedNotebook as NotebookBackendType;
268+
const savedNotebook = (notebookinfo as any).attributes.savedNotebook as NotebookBackendType;
269269
return response.ok({
270-
body: {
271-
...savedNotebook,
272-
isNotebookOwner: savedNotebook.owner
273-
? savedNotebook.owner === getUserName(request)
274-
: true,
275-
},
270+
body: savedNotebook,
276271
});
277272
} catch (error) {
278273
return response.custom({
@@ -306,7 +301,7 @@ export function registerNoteRoute(router: IRouter, auth: HttpAuth) {
306301
request.body.noteId
307302
);
308303
const createCloneNotebook = cloneNotebook(
309-
getNotebook.attributes.savedNotebook,
304+
(getNotebook as any).attributes.savedNotebook,
310305
request.body.name
311306
);
312307
const createdNotebook = await opensearchNotebooksClient.create(

0 commit comments

Comments
 (0)