Skip to content

Commit b0ba08c

Browse files
authored
Merge pull request #358 from TheauW/twartel-secure-import-app
feat: handle outdated imported states with automatic structure update
2 parents e429b91 + c4bf803 commit b0ba08c

13 files changed

Lines changed: 262 additions & 65 deletions

File tree

docs/developer/create_application.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ The applicatins created here will be available for DiracX-Web and for all the ex
66

77
### Declare the application
88

9-
In the file `packages/diracx-web-components/src/components/ApplicationList.ts` you can extend the `applicationList` with your new app. You must provide a name (explicit), the component representing the new app and an icon that will appear in the `Add application` menu. You can also give two functions, `setState` and `getState`, to configure the export and import of your app.
9+
In the file `packages/diracx-web-components/src/components/ApplicationList.ts` you can extend the `applicationList` with your new app.
10+
11+
You must provide:
12+
- A clear and explicit name
13+
- The component representing the new app
14+
- An icon that will appear in the `Add application` menu
15+
- An optional function, `validateAndConvertState`, which identifies and corrects the structure of a JSON pasted by the user during import. This function ensures compatibility between versions by transforming the pasted state into a valid, updated version. It should be reviewed and updated in any version that modifies the exported/imported state structure
16+
17+
💡You can look at the type `ApplicationMetadata` for more details
1018

1119
### Code the application
1220

docs/user/list_and_share_applications.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,5 @@ When managing multiple instances of the same application, grouping can help you
9797
1. **Share**: You can export the status of an app by clicking on the share button in the top-right corner of the screen. After clicking, you can select which group and app you want to share and then copy a text corresponding to the states of the selected applications.
9898

9999
2. **Import**: Next to the export button you can find the import button. You can paste into the window opened by the button the text corresponding to one or multiple shared apps. This will create a new group named *Imported App* with the imported applications and their settings.
100+
101+
**Good to know:** When switching to a new version, the settings you are trying to import may no longer be valid. In this case, a new window will appear, offering to resolve the issue by updating the state copied to your clipboard. This updated version preserves your imported rules as much as possible.

packages/diracx-web-components/src/components/ApplicationList.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import { FolderCopy, Monitor } from "@mui/icons-material";
44
import ApplicationMetadata from "../types/ApplicationMetadata";
5-
import JobMonitor from "./JobMonitor/JobMonitor";
5+
import JobMonitor, {
6+
validateAndConvertState as validateAndConvertState_JobMonitor,
7+
} from "./JobMonitor/JobMonitor";
68

79
export const applicationList: ApplicationMetadata[] = [
810
{
911
name: "Job Monitor",
1012
component: JobMonitor,
1113
icon: Monitor,
14+
validateAndConvertState: validateAndConvertState_JobMonitor,
1215
},
1316
{
1417
name: "File Catalog",

packages/diracx-web-components/src/components/DashboardLayout/DashboardDrawer.tsx

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -213,31 +213,40 @@ export default function DashboardDrawer({
213213
* @param appType - The type of the app to be created.
214214
*/
215215
const handleAppCreation = (appType: string) => {
216-
let group = userDashboard[userDashboard.length - 1];
217-
const empty = !group;
218-
if (empty) {
219-
//create a new group if there is no group
220-
group = {
221-
title: `Group ${userDashboard.length + 1}`,
222-
extended: false,
223-
items: [],
224-
};
216+
const group =
217+
userDashboard.length > 0
218+
? userDashboard[userDashboard.length - 1]
219+
: {
220+
// Create a new group if none exists
221+
title: `Group 1`,
222+
extended: false,
223+
items: [],
224+
};
225+
226+
const empty = userDashboard.length === 0;
227+
228+
const count = group.items.filter((item) =>
229+
item.title.startsWith(appType),
230+
).length;
231+
232+
let title = `${appType} ${count > 0 ? `${count}` : ""}`;
233+
while (group.items.some((app) => app.title === title)) {
234+
const match = title.match(/(\d+)$/);
235+
const num = match ? parseInt(match[1], 10) + 1 : undefined;
236+
title = `${appType} ${num}`;
225237
}
226238

227-
const count = userDashboard.reduce(
228-
(sum, group) =>
229-
sum + group.items.filter((item) => item.type === appType).length,
230-
0,
231-
);
232-
233-
const title = count > 0 ? `${appType} ${count + 1}` : `${appType}`;
234-
239+
let appId = `${appType} 0`;
240+
while (
241+
userDashboard.some((group) => group.items.some((app) => app.id === appId))
242+
) {
243+
const match = appId.match(/(\d+)$/);
244+
const num = match ? parseInt(match[1], 10) + 1 : 0;
245+
appId = `${appType} ${num}`;
246+
}
235247
const newApp = {
236248
title,
237-
id: `${title}${userDashboard.reduce(
238-
(sum, group) => sum + group.items.length,
239-
0,
240-
)}`,
249+
id: appId,
241250
type: appType,
242251
};
243252
group.items.push(newApp);
@@ -407,9 +416,9 @@ export default function DashboardDrawer({
407416
</Toolbar>
408417
{/* Map over user app instances and render them as list items in the drawer. */}
409418
<List>
410-
{userDashboard.map((group) => (
419+
{userDashboard.map((group, index) => (
411420
<ListItem
412-
key={group.title}
421+
key={index}
413422
disablePadding
414423
onContextMenu={handleContextMenu("group", group.title)}
415424
>

packages/diracx-web-components/src/components/DashboardLayout/DrawerItem.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
useTheme,
1111
TextField,
1212
} from "@mui/material";
13-
import { DragIndicator, SvgIconComponent, Apps } from "@mui/icons-material";
13+
import { DragIndicator, SvgIconComponent } from "@mui/icons-material";
1414
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
1515
import {
1616
draggable,
@@ -72,7 +72,7 @@ export default function DrawerItem({
7272

7373
const [, , appList, appId, setCurrentAppId] = useContext(ApplicationsContext);
7474
const { icon } = appList.find((app) => app.name === item.type) || {
75-
icon: Apps,
75+
icon: EggIcon,
7676
};
7777

7878
useEffect(() => {
@@ -194,7 +194,7 @@ export default function DrawerItem({
194194
selected={appId === item.id}
195195
>
196196
<ListItemIcon>
197-
<Icon component={icon ?? EggIcon} />
197+
<Icon component={icon} />
198198
</ListItemIcon>
199199
{renamingItemId === item.id ? (
200200
<TextField

packages/diracx-web-components/src/components/DashboardLayout/ExportButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export function ExportButton() {
119119
};
120120
});
121121

122-
setSelectedState(JSON.stringify(states, null, 2));
122+
setSelectedState(JSON.stringify(states));
123123
setDialogOpen(true);
124124
handleClose();
125125
};

packages/diracx-web-components/src/components/DashboardLayout/ImportButton.tsx

Lines changed: 145 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,40 @@ import {
1111
TextField,
1212
} from "@mui/material";
1313
import InputIcon from "@mui/icons-material/Input";
14-
import React, { useState, useContext, SetStateAction } from "react";
14+
import React, { useState, useContext, useEffect } from "react";
1515
import { ApplicationsContext } from "../../contexts";
16-
import { ApplicationMetadata, DashboardGroup } from "../../types";
16+
import {
17+
ApplicationMetadata,
18+
DashboardGroup,
19+
ApplicationSettings,
20+
} from "../../types";
1721
import { ApplicationState } from "../../types/ApplicationMetadata";
1822

1923
interface ImportDialogProps {
2024
open: boolean;
2125
onClose: () => void;
22-
onImport: (state: string) => void;
26+
onImport: (importedStates: ApplicationSettings[]) => void;
2327
}
2428

29+
/**
30+
* ImportDialog component allows users to import application states in JSON format.
31+
* It provides a text area for pasting the state and a button to import it.
32+
* @param open - Boolean indicating if the dialog is open.
33+
* @param onClose - Function to close the dialog.
34+
* @param onImport - Function to handle the import of the state.
35+
*/
2536
function ImportDialog({ open, onClose, onImport }: ImportDialogProps) {
2637
const [stateText, setStateText] = useState("");
2738
const [error, setError] = useState<string | null>(null);
2839

40+
useEffect(() => {
41+
setStateText("");
42+
setError(null);
43+
}, [open]);
44+
2945
const handleImport = () => {
3046
try {
31-
const parsedState = JSON.parse(stateText);
47+
const parsedState: ApplicationSettings[] = JSON.parse(stateText);
3248
onImport(parsedState);
3349
onClose();
3450
setStateText("");
@@ -74,28 +90,112 @@ function ImportDialog({ open, onClose, onImport }: ImportDialogProps) {
7490
);
7591
}
7692

93+
/**
94+
* DeprecatedStateDialog component displays a dialog with a list of applications
95+
* that have an outdated state format. It allows users to copy the updated state.
96+
* @param open - Boolean indicating if the dialog is open.
97+
* @param onClose - Function to close the dialog.
98+
* @param correctStates - Array of objects containing the old and new state of the applications.
99+
*/
100+
function DeprecatedStateDialog({
101+
open,
102+
onClose,
103+
correctStates,
104+
}: {
105+
open: boolean;
106+
onClose: () => void;
107+
correctStates: {
108+
oldSettings: ApplicationSettings;
109+
newState: ApplicationState;
110+
}[];
111+
}) {
112+
const handleCopy = (app: {
113+
oldSettings: ApplicationSettings;
114+
newState: ApplicationState;
115+
}) => {
116+
const formatted = {
117+
appType: app.oldSettings.appType,
118+
appName: app.oldSettings.appName,
119+
state: app.newState,
120+
};
121+
navigator.clipboard.writeText(JSON.stringify(formatted));
122+
};
123+
124+
const handleCopyAll = () => {
125+
const allFormatted = correctStates.map(({ oldSettings, newState }) => ({
126+
appType: oldSettings.appType,
127+
appName: oldSettings.appName,
128+
state: newState,
129+
}));
130+
navigator.clipboard.writeText(JSON.stringify(allFormatted));
131+
};
132+
133+
return (
134+
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
135+
<DialogTitle>Outdated State Format Detected</DialogTitle>
136+
<DialogContent>
137+
<p>
138+
Some imported applications use an outdated format that will soon no
139+
longer be supported.
140+
<br />
141+
Please update them now by copying their updated version.
142+
</p>
143+
144+
<ul>
145+
{correctStates.map(({ oldSettings }, index) => (
146+
<li key={index} style={{ marginTop: "0.5em" }}>
147+
<b>{oldSettings.appName}</b> ({oldSettings.appType}){" "}
148+
<Button
149+
size="small"
150+
onClick={() => handleCopy(correctStates[index])}
151+
sx={{ ml: 1 }}
152+
>
153+
Copy
154+
</Button>
155+
</li>
156+
))}
157+
</ul>
158+
</DialogContent>
159+
<DialogActions>
160+
<Button onClick={onClose}>Cancel</Button>
161+
<Button variant="contained" onClick={handleCopyAll}>
162+
Copy All Updated JSON
163+
</Button>
164+
</DialogActions>
165+
</Dialog>
166+
);
167+
}
168+
77169
/**
78170
* ImportButton component allows users to import the state of applications.
79171
* It provides a dialog to paste the state in JSON format.
80172
*/
81173
export function ImportButton() {
82174
const [dialogOpen, setDialogOpen] = useState(false);
175+
const [correctStates, setCorrectStates] = useState<
176+
{ oldSettings: ApplicationSettings; newState: ApplicationState }[]
177+
>([]);
83178
const [userDashboard, setUserDashboard, appList] =
84179
useContext(ApplicationsContext);
85180

86-
const handleImport = (importedState: ApplicationState) => {
87-
const states = Array.isArray(importedState)
88-
? importedState
89-
: [importedState];
181+
const handleImport = (
182+
importedStates: ApplicationSettings | ApplicationSettings[],
183+
) => {
184+
const states = Array.isArray(importedStates)
185+
? importedStates
186+
: [importedStates];
187+
188+
const count = userDashboard.filter((group) =>
189+
group.title.startsWith("Imported Applications"),
190+
).length;
90191

91-
const id: number = userDashboard.reduce(
92-
(acc, group) =>
93-
group.title.startsWith("Imported Applications") ? acc + 1 : acc,
94-
0,
95-
);
192+
let title = `Imported Applications${count > 0 ? ` ${count}` : ""}`;
193+
while (userDashboard.some((group) => group.title === title)) {
194+
title = `Imported Applications ${parseInt(title.split(" ")[2]) + 1}`;
195+
}
96196

97197
const newGroup = {
98-
title: `Imported Applications${id > 0 ? ` (${id})` : ""}`,
198+
title: title,
99199
extended: true,
100200
items: [],
101201
};
@@ -106,14 +206,23 @@ export function ImportButton() {
106206

107207
states.forEach((state) => {
108208
if (state.state !== "null") {
209+
const applicationMetadata = appList.find(
210+
(app) => app.name === state.appType,
211+
);
109212
const appId = handleAppCreation(
110213
state.appType,
111214
state.appName,
112-
appList,
215+
applicationMetadata,
113216
userDashboard,
114217
setUserDashboard,
115218
);
116-
sessionStorage.setItem(`${appId}_State`, state.state);
219+
const [appState, haveChangedState] =
220+
applicationMetadata?.validateAndConvertState
221+
? applicationMetadata.validateAndConvertState(state.state)
222+
: [state.state, false];
223+
if (haveChangedState)
224+
correctStates.push({ oldSettings: state, newState: appState });
225+
sessionStorage.setItem(`${appId}_State`, appState);
117226
} else {
118227
console.warn(`No state to import for app type: ${state.appType}`);
119228
}
@@ -136,6 +245,12 @@ export function ImportButton() {
136245
onClose={() => setDialogOpen(false)}
137246
onImport={handleImport}
138247
/>
248+
249+
<DeprecatedStateDialog
250+
open={correctStates.length > 0}
251+
onClose={() => setCorrectStates([])}
252+
correctStates={correctStates}
253+
/>
139254
</>
140255
);
141256
}
@@ -153,24 +268,26 @@ export function ImportButton() {
153268
function handleAppCreation(
154269
appType: string,
155270
appTitle: string,
156-
appList: ApplicationMetadata[],
271+
applicationMetadata: ApplicationMetadata | undefined,
157272
userDashboard: DashboardGroup[],
158-
setUserDashboard: React.Dispatch<SetStateAction<DashboardGroup[]>>,
273+
setUserDashboard: React.Dispatch<React.SetStateAction<DashboardGroup[]>>,
159274
): string {
160275
const group = userDashboard[userDashboard.length - 1];
161276

162-
const count = userDashboard.reduce(
163-
(sum, group) =>
164-
sum +
165-
group.items.filter((item) => item.title.startsWith(appTitle)).length,
166-
0,
167-
);
277+
const count = group.items.filter((item) =>
278+
item.title.startsWith(appTitle),
279+
).length;
168280

169281
const title = count > 0 ? `${appTitle} (${count + 1})` : appTitle;
170-
const appId = `${title}${userDashboard.reduce(
171-
(sum, group) => sum + group.items.length,
172-
0,
173-
)}`;
282+
283+
let appId = `${appType} 0`;
284+
while (
285+
userDashboard.some((group) => group.items.some((app) => app.id === appId))
286+
) {
287+
const match = appId.match(/(\d+)$/);
288+
const num = match ? parseInt(match[1], 10) + 1 : 0;
289+
appId = `${appType} ${num}`;
290+
}
174291

175292
const newApp = {
176293
title: title,

0 commit comments

Comments
 (0)