Skip to content

Commit a504a35

Browse files
committed
feat: add share and import features
1 parent f2f2a55 commit a504a35

13 files changed

Lines changed: 792 additions & 29 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material";
44
import ApplicationMetadata from "../types/ApplicationMetadata";
5-
import JobMonitor from "./JobMonitor/JobMonitor";
5+
import JobMonitor, { exportState, setState } from "./JobMonitor/JobMonitor";
66
import BaseApp from "./BaseApp/BaseApp";
77

88
export const applicationList: ApplicationMetadata[] = [
@@ -11,6 +11,8 @@ export const applicationList: ApplicationMetadata[] = [
1111
name: "Job Monitor",
1212
component: JobMonitor,
1313
icon: Monitor,
14+
getState: exportState,
15+
setState: setState,
1416
},
1517
{
1618
name: "File Catalog",

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
import { ProfileButton } from "./ProfileButton";
1616
import { ThemeToggleButton } from "./ThemeToggleButton";
1717
import DashboardDrawer from "./DashboardDrawer";
18+
import { ShareButton } from "./ShareButton";
19+
import { ImportButton } from "./ImportButton";
1820

1921
interface DashboardProps {
2022
/** The content to be displayed in the main area */
@@ -121,6 +123,8 @@ export default function Dashboard({
121123
}}
122124
>
123125
<Stack direction="row" spacing={1} alignItems="center">
126+
<ImportButton />
127+
<ShareButton />
124128
<ThemeToggleButton />
125129
<ProfileButton />
126130
</Stack>

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,21 @@ export default function DashboardDrawer({
291291

292292
const handleDelete = () => {
293293
if (contextState.type === "group") {
294+
const group = userDashboard.find(
295+
(group) => group.title === contextState.id,
296+
);
297+
if (group) {
298+
group.items.forEach((item) => {
299+
sessionStorage.removeItem(`${item.id}_State`);
300+
});
301+
}
302+
294303
setUserDashboard((userDashboard) =>
295304
userDashboard.filter((group) => group.title !== contextState.id),
296305
);
297306
} else if (contextState.type === "item") {
307+
sessionStorage.removeItem(`${contextState.id}_State`);
308+
298309
setUserDashboard((userDashboard) =>
299310
userDashboard.map((group) => {
300311
const newItems = group.items.filter(

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,16 @@ export default function DrawerItemGroup({
9191
// Handle renaming of the group
9292
const handleGroupRename = () => {
9393
if (renameValue.trim() === "") return;
94-
setUserDashboard((groups) =>
95-
groups.map((group) =>
96-
group.title === title ? { ...group, title: renameValue } : group,
97-
),
98-
);
94+
setUserDashboard((groups) => {
95+
const count = groups.reduce(
96+
(sum, group) => (group.title.startsWith(renameValue) ? sum + 1 : sum),
97+
0,
98+
);
99+
const newTitle = count > 0 ? `${renameValue} (${count})` : renameValue;
100+
return groups.map((group) =>
101+
group.title === title ? { ...group, title: newTitle } : group,
102+
);
103+
});
99104
setRenamingGroupId(null);
100105
setRenameValue("");
101106
};
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"use client";
2+
3+
import {
4+
IconButton,
5+
Tooltip,
6+
Dialog,
7+
DialogTitle,
8+
DialogContent,
9+
DialogActions,
10+
Button,
11+
TextField,
12+
} from "@mui/material";
13+
import InputIcon from "@mui/icons-material/Input";
14+
import React, { useState, useContext, SetStateAction } from "react";
15+
import { ApplicationsContext } from "../../contexts";
16+
import { ApplicationMetadata, DashboardGroup } from "../../types";
17+
import { ApplicationState } from "../../types/ApplicationMetadata";
18+
19+
interface ImportDialogProps {
20+
open: boolean;
21+
onClose: () => void;
22+
onImport: (state: string) => void;
23+
}
24+
25+
function ImportDialog({ open, onClose, onImport }: ImportDialogProps) {
26+
const [stateText, setStateText] = useState("");
27+
const [error, setError] = useState<string | null>(null);
28+
29+
const handleImport = () => {
30+
try {
31+
const parsedState = JSON.parse(stateText);
32+
onImport(parsedState);
33+
onClose();
34+
setStateText("");
35+
setError(null);
36+
} catch {
37+
setError("Invalid JSON format");
38+
}
39+
};
40+
41+
return (
42+
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
43+
<DialogTitle data-testid="import-menu">
44+
Import Application State
45+
</DialogTitle>
46+
<DialogContent>
47+
<TextField
48+
multiline
49+
rows={8}
50+
fullWidth
51+
value={stateText}
52+
onChange={(e) => setStateText(e.target.value)}
53+
placeholder="Paste your application state here..."
54+
error={!!error}
55+
helperText={error}
56+
sx={{ mt: 2 }}
57+
datatype="import-menu-field"
58+
/>
59+
</DialogContent>
60+
<DialogActions>
61+
<Button onClick={onClose}>Cancel</Button>
62+
<Button
63+
onClick={handleImport}
64+
variant="contained"
65+
disabled={!stateText.trim()}
66+
>
67+
Import
68+
</Button>
69+
</DialogActions>
70+
</Dialog>
71+
);
72+
}
73+
74+
/**
75+
* ImportButton component allows users to import the state of applications.
76+
* It provides a dialog to paste the state in JSON format.
77+
*/
78+
export function ImportButton() {
79+
const [dialogOpen, setDialogOpen] = useState(false);
80+
const [userDashboard, setUserDashboard, appList] =
81+
useContext(ApplicationsContext);
82+
83+
const handleImport = (importedState: ApplicationState) => {
84+
const states = Array.isArray(importedState)
85+
? importedState
86+
: [importedState];
87+
88+
const id: number = userDashboard.reduce(
89+
(acc, group) =>
90+
group.title.startsWith("Imported Applications") ? acc + 1 : acc,
91+
0,
92+
);
93+
94+
const newGroup = {
95+
title: `Imported Applications${id > 0 ? ` (${id})` : ""}`,
96+
extended: true,
97+
items: [],
98+
};
99+
100+
userDashboard.push(newGroup);
101+
102+
states.forEach((state) => {
103+
const appMetadata = appList.find((meta) => meta.name === state.appType);
104+
if (state.state !== "null" && appMetadata?.setState) {
105+
const appId = handleAppCreation(
106+
state.appType,
107+
appList,
108+
userDashboard,
109+
setUserDashboard,
110+
);
111+
appMetadata.setState(appId, state.state);
112+
} else {
113+
if (state.state === "null")
114+
console.warn(`No state to import for app type: ${state.appType}`);
115+
else console.warn(`No setState handler for app type: ${state.appType}`);
116+
}
117+
});
118+
};
119+
120+
return (
121+
<>
122+
<Tooltip title="Import application state">
123+
<IconButton onClick={() => setDialogOpen(true)}>
124+
<InputIcon />
125+
</IconButton>
126+
</Tooltip>
127+
128+
<ImportDialog
129+
open={dialogOpen}
130+
onClose={() => setDialogOpen(false)}
131+
onImport={handleImport}
132+
/>
133+
</>
134+
);
135+
}
136+
137+
/**
138+
* Handles the creation of a new app in the dashboard drawer.
139+
*
140+
* @param appType - The type of the app to be created.
141+
* @param icon - The icon component for the app.
142+
*/
143+
function handleAppCreation(
144+
appType: string,
145+
appList: ApplicationMetadata[],
146+
userDashboard: DashboardGroup[],
147+
setUserDashboard: React.Dispatch<SetStateAction<DashboardGroup[]>>,
148+
): string {
149+
const group = userDashboard[userDashboard.length - 1];
150+
151+
let title = `${appType} ${userDashboard.reduce(
152+
(sum, group) =>
153+
sum + group.items.filter((item) => item.type === appType).length,
154+
1,
155+
)}`;
156+
while (group.items.some((item) => item.title === title)) {
157+
title = `${appType} ${parseInt(title.split(" ")[1]) + 1}`;
158+
}
159+
160+
const appId = `${title}${userDashboard.reduce(
161+
(sum, group) => sum + group.items.length,
162+
0,
163+
)}`;
164+
165+
const newApp = {
166+
title,
167+
id: appId,
168+
type: appType,
169+
icon: appList.find((app) => app.name === appType)?.icon || null,
170+
};
171+
group.items.push(newApp);
172+
setUserDashboard((userDashboard) =>
173+
userDashboard.map((g) => (g.title === group.title ? group : g)),
174+
);
175+
176+
return appId;
177+
}

0 commit comments

Comments
 (0)