Skip to content

Commit 51d243c

Browse files
authored
Merge pull request #340 from TheauW/twartel-share-app-state
feat: add share and import features
2 parents 10f94a9 + 502f543 commit 51d243c

15 files changed

Lines changed: 799 additions & 29 deletions

File tree

docs/developer/contribute.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,18 @@
1616
- **Code Documentation:** Ensure that any code you write is well-documented. This includes:
1717
- Inline comments where necessary to explain complex logic.
1818
- Updating or creating Storybook documentation if you are contributing to the `diracx-web-components` library.
19-
- **Writing/Updating Tests:** When you change or add new code, make sure to write or update tests accordingly. This helps maintain the reliability and stability of the codebase.
19+
- You can use tools like [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) to maintain code quality.
20+
21+
- **Testing**:
22+
23+
- **Component Testing**: Write tests for your components to ensure they work as expected. Use [Jest](https://jestjs.io/) for unit testing and snapshot testing of your React components.
24+
- **Application Testing**: Use [Cypress](https://www.cypress.io/) for end-to-end testing to simulate real user interactions and ensure your application behaves correctly.
25+
- **Test Coverage**: Maintain good test coverage to ensure that your critical features are well-protected during updates. Tools like Jest provide [coverage reports](https://jestjs.io/docs/code-coverage) that help you identify untested parts of your code.
26+
27+
28+
- **Accessibility**: Make your application accessible to all users. Use semantic HTML, ARIA attributes, and test your application with different screen sizes and assistive technologies.
29+
30+
By following these practices, you'll ensure that your codebase remains robust, secure, and maintainable.
2031

2132
**Good to know:** If you create an export function or component in `diracx-web-components`, you must add it to the `index.ts` file and run `npm run build` inside `packages/diracx-web-components` to ensure the pre-commit hook passes.
2233

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Create an application
2+
3+
## In DiracX-Web
4+
5+
The applicatins created here will be available for DiracX-Web and for all the extensions.
6+
7+
### Declare the application
8+
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.
10+
11+
### Code the application
12+
13+
The code of your app should be in `packages/diracx-web-components/src/components/<new-app>`. The new app can and should use what already exist in `@dirac-grid/diracx-web-components`.
14+
15+
In order to be compatible with the share and import buttons, the application must write its state to the session storage at `<appId>_State`. This slot is read from and written to by the corresponding functions.
16+
17+
💡You can look at `JobMonitor` as an example.

docs/user/list_and_share_applications.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,10 @@ When managing multiple instances of the same application, grouping can help you
9090
- :bulb: A context menu will appear.
9191
2. Select **Delete**.
9292
- :bulb: The group and all its application instances will disappear from the sidebar.
93+
94+
95+
### Share and import the settings of an application
96+
97+
1. **Share**: You can share 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.
98+
99+
2. **Import**: Next to the share 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.

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+
// Create a group only if there are valid states to import
101+
if (states.some((state) => state.state !== "null"))
102+
userDashboard.push(newGroup);
103+
104+
states.forEach((state) => {
105+
if (state.state !== "null") {
106+
const appId = handleAppCreation(
107+
state.appType,
108+
state.appName,
109+
appList,
110+
userDashboard,
111+
setUserDashboard,
112+
);
113+
sessionStorage.setItem(`${appId}_State`, state.state);
114+
} else {
115+
console.warn(`No state to import 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+
appTitle: string,
146+
appList: ApplicationMetadata[],
147+
userDashboard: DashboardGroup[],
148+
setUserDashboard: React.Dispatch<SetStateAction<DashboardGroup[]>>,
149+
): string {
150+
const group = userDashboard[userDashboard.length - 1];
151+
152+
const count = userDashboard.reduce(
153+
(sum, group) =>
154+
sum +
155+
group.items.filter((item) => item.title.startsWith(appTitle)).length,
156+
0,
157+
);
158+
159+
const title = count > 0 ? `${appTitle} (${count + 1})` : appTitle;
160+
const appId = `${title}${userDashboard.reduce(
161+
(sum, group) => sum + group.items.length,
162+
0,
163+
)}`;
164+
165+
const newApp = {
166+
title: 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)