Skip to content

Commit b1a5148

Browse files
committed
[frontend] Apply commons to custom dashboards
1 parent 17e2aeb commit b1a5148

File tree

1 file changed

+58
-261
lines changed
  • opencti-platform/opencti-front/src/private/components/workspaces/dashboards

1 file changed

+58
-261
lines changed

opencti-platform/opencti-front/src/private/components/workspaces/dashboards/Dashboard.jsx

Lines changed: 58 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
import { useEffect, useMemo, useState } from 'react';
2-
import * as R from 'ramda';
31
import { graphql, useFragment } from 'react-relay';
4-
import ReactGridLayout, { useContainerWidth } from 'react-grid-layout';
5-
import { v4 as uuid } from 'uuid';
6-
import { Stack, Box } from '@mui/material';
7-
import { useTheme } from '@mui/styles';
8-
import DashboardTimeFilters from './DashboardTimeFilters';
2+
import Stack from '@mui/material/Stack';
3+
import DashboardTimeFilters from '../../../../components/dashboard/DashboardTimeFilters';
94
import WorkspaceHeader from '../workspaceHeader/WorkspaceHeader';
10-
import { commitMutation, handleError } from '../../../../relay/environment';
5+
import { commitMutation, handleError, fetchQuery } from '../../../../relay/environment';
116
import { workspaceMutationFieldPatch } from '../WorkspaceEditionOverview';
127
import useGranted, { EXPLORE_EXUPDATE } from '../../../../utils/hooks/useGranted';
13-
import WorkspaceWidgetPopover from './WorkspaceWidgetPopover';
14-
import { fromB64, toB64 } from '../../../../utils/String';
15-
import { deserializeDashboardManifestForFrontend, serializeDashboardManifestForBackend } from '../../../../utils/filters/filtersUtils';
168
import useApiMutation from '../../../../utils/hooks/useApiMutation';
17-
import DashboardViz from './DashboardViz';
9+
import DashboardContent from '../../../../components/dashboard/DashboardContent';
10+
import useDashboard from '../../../../components/dashboard/useDashboard';
11+
12+
const dashboardExportWidgetQuery = graphql`
13+
query DashboardWidgetExportQuery($id: String!, $widgetId: ID!) {
14+
workspace(id: $id) {
15+
toWidgetExport(widgetId: $widgetId)
16+
}
17+
}
18+
`;
1819

1920
const dashboardLayoutMutation = graphql`
2021
mutation DashboardLayoutMutation($id: ID!, $input: [EditInput!]!) {
@@ -54,142 +55,40 @@ const dashboardFragment = graphql`
5455
}
5556
`;
5657

57-
const DashboardComponent = ({ data, noToolbar = false }) => {
58-
const theme = useTheme();
59-
const [commitWidgetImportMutation] = useApiMutation(dashboardImportWidgetMutation);
58+
const onExportWidget = async (id, widget) => {
59+
const data = await fetchQuery(dashboardExportWidgetQuery, { id, widgetId: widget.id })
60+
.toPromise();
61+
return data.workspace?.toWidgetExport;
62+
};
6063

64+
const DashboardComponent = ({ data, noToolbar = false }) => {
6165
const workspace = useFragment(dashboardFragment, data);
62-
const { width, containerRef } = useContainerWidth();
63-
64-
const [deleting, setDeleting] = useState(false);
65-
const [idToResize, setIdToResize] = useState();
66-
const handleResize = (updatedWidget) => setIdToResize(updatedWidget);
66+
const [commitWidgetImportMutation] = useApiMutation(dashboardImportWidgetMutation);
6767

6868
const userHasEditAccess = workspace.currentUserAccessRight === 'admin'
6969
|| workspace.currentUserAccessRight === 'edit';
7070
const userHasUpdateCapa = useGranted([EXPLORE_EXUPDATE]);
7171
const userCanEdit = userHasEditAccess && userHasUpdateCapa;
7272

73-
useEffect(() => {
74-
const timeout = setTimeout(() => {
75-
window.dispatchEvent(new Event('resize'));
76-
}, 1200);
77-
return () => {
78-
clearTimeout(timeout);
79-
};
80-
}, []);
81-
82-
// Map of widget layouts, refreshed when workspace is updated (thanks to useMemo below).
83-
// We use a local map of layouts to avoid a lot of computation when only changing position
84-
// or dimension of widgets.
85-
const [widgetsLayouts, setWidgetsLayouts] = useState({});
86-
87-
// Deserialized manifest, refreshed when workspace is updated.
88-
const manifest = useMemo(() => {
89-
return workspace.manifest && workspace.manifest.length > 0
90-
? deserializeDashboardManifestForFrontend(fromB64(workspace.manifest))
91-
: { widgets: {}, config: {} };
92-
}, [workspace.manifest]);
93-
94-
// Array of all widgets, refreshed when workspace is updated.
95-
const widgetsArray = useMemo(() => {
96-
return Object.values(manifest.widgets);
97-
}, [manifest]);
98-
99-
useEffect(() => {
100-
setWidgetsLayouts(
101-
widgetsArray.reduce((res, widget) => {
102-
res[widget.id] = widget.layout;
103-
return res;
104-
}, {}),
105-
);
106-
}, [widgetsArray]);
107-
108-
/**
109-
* Merge a manifest with some layouts and transform it in base64.
110-
*
111-
* @param newManifest Manifest to merge with local changes and stringify.
112-
* @param layouts Local layout changes.
113-
* @returns {string} Manifest in B64.
114-
*/
115-
const prepareManifest = (newManifest, layouts) => {
116-
// Need to sync manifest with local layouts before sending for update.
117-
// A desync occurs when resizing or moving a widget because in those cases
118-
// we skip a complete reload to avoid performance issue.
119-
const syncWidgets = Object.values(newManifest.widgets).reduce((res, widget) => {
120-
const localLayout = layouts[widget.id];
121-
res[widget.id] = {
122-
...widget,
123-
layout: localLayout || widget.layout,
124-
};
125-
return res;
126-
}, {});
127-
const manifestToSave = {
128-
...newManifest,
129-
widgets: syncWidgets,
130-
};
131-
132-
const strManifest = serializeDashboardManifestForBackend(manifestToSave);
133-
return toB64(strManifest);
134-
};
135-
136-
const saveManifest = (newManifest, opts = { layouts: widgetsLayouts, noRefresh: false }) => {
137-
const { layouts, noRefresh } = opts;
138-
const newManifestEncoded = prepareManifest(newManifest, layouts);
139-
// Sometimes (in case of layout adjustment) we do not want to re-fetch
140-
// all the manifest because widgets data is still the same, and it's costly
141-
// in performance.
73+
const onSave = (id, newManifestEncoded, noRefresh, onCompleted) => {
14274
const mutation = noRefresh ? dashboardLayoutMutation : workspaceMutationFieldPatch;
143-
if (workspace.manifest !== newManifestEncoded) {
144-
commitMutation({
145-
mutation,
146-
variables: {
147-
id: workspace.id,
148-
input: {
149-
key: 'manifest',
150-
value: newManifestEncoded,
151-
},
152-
},
153-
onCompleted: () => {
154-
setDeleting(false);
75+
commitMutation({
76+
mutation,
77+
variables: {
78+
id,
79+
input: {
80+
key: 'manifest',
81+
value: newManifestEncoded,
15582
},
156-
});
157-
}
158-
};
159-
160-
const handleDateChange = (type, value) => {
161-
let newManifest = R.assoc(
162-
'config',
163-
R.assoc(type, value === 'none' ? null : value, manifest.config),
164-
manifest,
165-
);
166-
if (type === 'relativeDate' && value !== 'none') {
167-
newManifest = R.assoc(
168-
'config',
169-
R.assoc('startDate', null, newManifest.config),
170-
newManifest,
171-
);
172-
newManifest = R.assoc(
173-
'config',
174-
R.assoc('endDate', null, newManifest.config),
175-
newManifest,
176-
);
177-
}
178-
saveManifest(newManifest);
179-
};
180-
181-
const getNextRow = () => {
182-
return widgetsArray.reduce((max, { layout }) => {
183-
const widgetEndRow = layout.y + layout.h;
184-
return widgetEndRow > max ? widgetEndRow : max;
185-
}, 0);
83+
},
84+
onCompleted,
85+
});
18686
};
18787

188-
const importWidget = (widgetConfig) => {
189-
const manifestEncoded = prepareManifest(manifest, widgetsLayouts);
88+
const onImportWidget = (id, widgetConfig, manifestEncoded) => {
19089
commitWidgetImportMutation({
19190
variables: {
192-
id: workspace.id,
91+
id,
19392
input: {
19493
importType: 'widget',
19594
file: widgetConfig,
@@ -202,134 +101,32 @@ const DashboardComponent = ({ data, noToolbar = false }) => {
202101
});
203102
};
204103

205-
const handleAddWidget = (widgetConfig) => {
206-
saveManifest({
207-
...manifest,
208-
widgets: {
209-
...manifest.widgets,
210-
[widgetConfig.id]: {
211-
...widgetConfig,
212-
layout: {
213-
i: widgetConfig.id,
214-
x: 0,
215-
y: getNextRow(),
216-
w: 4,
217-
h: 2,
218-
},
219-
},
220-
},
221-
});
222-
};
223-
224-
const handleUpdateWidget = (widgetManifest) => {
225-
const newManifest = {
226-
...manifest,
227-
widgets: { ...manifest.widgets, [widgetManifest.id]: widgetManifest },
228-
};
229-
saveManifest(newManifest);
230-
};
231-
232-
const handleDeleteWidget = (widgetId) => {
233-
setDeleting(true);
234-
const newWidgets = { ...manifest.widgets };
235-
delete newWidgets[widgetId];
236-
saveManifest({
237-
...manifest,
238-
widgets: newWidgets,
239-
});
240-
};
241-
242-
const handleDuplicateWidget = (widgetToDuplicate) => {
243-
handleAddWidget({
244-
...widgetToDuplicate,
245-
id: uuid(),
246-
});
247-
};
248-
249-
const onLayoutChange = (layouts) => {
250-
if (deleting) return;
251-
252-
const newLayouts = layouts.reduce((res, layout) => {
253-
res[layout.i] = layout;
254-
return res;
255-
}, {});
256-
257-
if (R.equals(newLayouts, widgetsLayouts)) return; // ⛔ prevent loop
258-
259-
setWidgetsLayouts(newLayouts);
260-
saveManifest(manifest, { layouts: newLayouts, noRefresh: true });
261-
};
262-
104+
const helpers = useDashboard({ entity: workspace, onSave, onImportWidget, onExportWidget });
105+
const { handleAddWidget, handleImportWidget, handleDateChange, config } = helpers;
263106
return (
264-
<Box
265-
id="container"
266-
ref={containerRef}
267-
sx={{
268-
'& .react-grid-item.react-grid-placeholder': {
269-
border: `2px solid ${theme.palette.primary.main}`,
270-
borderRadius: 1,
271-
},
272-
}}
273-
>
274-
<Stack gap={2}>
275-
{!noToolbar && (
276-
<Stack gap={1}>
277-
<WorkspaceHeader
278-
handleAddWidget={handleAddWidget}
279-
handleImportWidget={importWidget}
280-
data={workspace}
281-
variant="dashboard"
282-
/>
283-
<DashboardTimeFilters
284-
currentUserAccessRight={workspace.currentUserAccessRight}
285-
config={manifest.config}
286-
handleDateChange={handleDateChange}
287-
/>
288-
</Stack>
289-
)}
290-
<ReactGridLayout
291-
className="layout"
292-
width={width}
293-
layout={Object.values(widgetsLayouts)}
294-
gridConfig={{ margin: [20, 20], rowHeight: 50, cols: 12, containerPadding: [0, 0] }}
295-
dragConfig={{ enabled: userCanEdit ? !noToolbar : false, cancel: '.noDrag' }}
296-
resizeConfig={{ enabled: userCanEdit ? !noToolbar : false }}
297-
onLayoutChange={userCanEdit && !noToolbar ? onLayoutChange : () => true}
298-
onResizeStart={userCanEdit ? (_, { i }) => handleResize(i) : undefined}
299-
onResizeStop={userCanEdit ? handleResize : undefined}
300-
>
301-
{widgetsArray.map((widget) => {
302-
if (!widgetsLayouts[widget.id]) return null;
303-
const popover = userCanEdit && !noToolbar && (
304-
<WorkspaceWidgetPopover
305-
widget={widget}
306-
workspace={workspace}
307-
onUpdate={handleUpdateWidget}
308-
onDuplicate={handleDuplicateWidget}
309-
onDelete={() => handleDeleteWidget(widget.id)}
310-
/>
311-
);
312-
313-
return (
314-
<div
315-
key={widget.id}
316-
style={{
317-
display: 'relative',
318-
}}
319-
>
320-
{widget.id === idToResize ? <div /> : (
321-
<DashboardViz
322-
widget={widget}
323-
config={manifest.config}
324-
popover={popover}
325-
/>
326-
)}
327-
</div>
328-
);
329-
})}
330-
</ReactGridLayout>
331-
</Stack>
332-
</Box>
107+
<Stack gap={2}>
108+
{!noToolbar && (
109+
<Stack gap={1}>
110+
<WorkspaceHeader
111+
handleAddWidget={handleAddWidget}
112+
handleImportWidget={handleImportWidget}
113+
data={workspace}
114+
variant="dashboard"
115+
/>
116+
<DashboardTimeFilters
117+
currentUserAccessRight={workspace.currentUserAccessRight}
118+
config={config}
119+
handleDateChange={handleDateChange}
120+
/>
121+
</Stack>
122+
)
123+
}
124+
<DashboardContent
125+
helpers={helpers}
126+
isEditable={userCanEdit}
127+
dashboardEntity={workspace}
128+
/>
129+
</Stack>
333130
);
334131
};
335132

0 commit comments

Comments
 (0)