Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion docs/developer/contribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@
- **Code Documentation:** Ensure that any code you write is well-documented. This includes:
- Inline comments where necessary to explain complex logic.
- Updating or creating Storybook documentation if you are contributing to the `diracx-web-components` library.
- **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.
- You can use tools like [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) to maintain code quality.

- **Testing**:

- **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.
- **Application Testing**: Use [Cypress](https://www.cypress.io/) for end-to-end testing to simulate real user interactions and ensure your application behaves correctly.
- **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.


- **Accessibility**: Make your application accessible to all users. Use semantic HTML, ARIA attributes, and test your application with different screen sizes and assistive technologies.

By following these practices, you'll ensure that your codebase remains robust, secure, and maintainable.

**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.

Expand Down
17 changes: 17 additions & 0 deletions docs/developer/create_application.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Create an application

## In DiracX-Web

The applicatins created here will be available for DiracX-Web and for all the extensions.

### Declare the application

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.

### Code the application

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`.

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.

💡You can look at `JobMonitor` as an example.
7 changes: 7 additions & 0 deletions docs/user/list_and_share_applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,10 @@ When managing multiple instances of the same application, grouping can help you
- :bulb: A context menu will appear.
2. Select **Delete**.
- :bulb: The group and all its application instances will disappear from the sidebar.


### Share and import the settings of an application

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.

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.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import { ProfileButton } from "./ProfileButton";
import { ThemeToggleButton } from "./ThemeToggleButton";
import DashboardDrawer from "./DashboardDrawer";
import { ShareButton } from "./ShareButton";
import { ImportButton } from "./ImportButton";

interface DashboardProps {
/** The content to be displayed in the main area */
Expand Down Expand Up @@ -121,6 +123,8 @@ export default function Dashboard({
}}
>
<Stack direction="row" spacing={1} alignItems="center">
<ImportButton />
<ShareButton />
<ThemeToggleButton />
<ProfileButton />
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,21 @@ export default function DashboardDrawer({

const handleDelete = () => {
if (contextState.type === "group") {
const group = userDashboard.find(
(group) => group.title === contextState.id,
);
if (group) {
group.items.forEach((item) => {
sessionStorage.removeItem(`${item.id}_State`);
});
}

setUserDashboard((userDashboard) =>
userDashboard.filter((group) => group.title !== contextState.id),
);
} else if (contextState.type === "item") {
sessionStorage.removeItem(`${contextState.id}_State`);

setUserDashboard((userDashboard) =>
userDashboard.map((group) => {
const newItems = group.items.filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,16 @@ export default function DrawerItemGroup({
// Handle renaming of the group
const handleGroupRename = () => {
if (renameValue.trim() === "") return;
setUserDashboard((groups) =>
groups.map((group) =>
group.title === title ? { ...group, title: renameValue } : group,
),
);
setUserDashboard((groups) => {
const count = groups.reduce(
(sum, group) => (group.title.startsWith(renameValue) ? sum + 1 : sum),
0,
);
const newTitle = count > 0 ? `${renameValue} (${count})` : renameValue;
return groups.map((group) =>
group.title === title ? { ...group, title: newTitle } : group,
);
});
setRenamingGroupId(null);
setRenameValue("");
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"use client";

import {
IconButton,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
} from "@mui/material";
import InputIcon from "@mui/icons-material/Input";
import React, { useState, useContext, SetStateAction } from "react";
import { ApplicationsContext } from "../../contexts";
import { ApplicationMetadata, DashboardGroup } from "../../types";
import { ApplicationState } from "../../types/ApplicationMetadata";

interface ImportDialogProps {
open: boolean;
onClose: () => void;
onImport: (state: string) => void;
}

function ImportDialog({ open, onClose, onImport }: ImportDialogProps) {
const [stateText, setStateText] = useState("");
const [error, setError] = useState<string | null>(null);

const handleImport = () => {
try {
const parsedState = JSON.parse(stateText);
onImport(parsedState);
onClose();
setStateText("");
setError(null);
} catch {
setError("Invalid JSON format");
}
};

return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle data-testid="import-menu">
Import Application State
</DialogTitle>
<DialogContent>
<TextField
multiline
rows={8}
fullWidth
value={stateText}
onChange={(e) => setStateText(e.target.value)}
placeholder="Paste your application state here..."
error={!!error}
helperText={error}
sx={{ mt: 2 }}
datatype="import-menu-field"
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button
onClick={handleImport}
variant="contained"
disabled={!stateText.trim()}
>
Import
</Button>
</DialogActions>
</Dialog>
);
}

/**
* ImportButton component allows users to import the state of applications.
* It provides a dialog to paste the state in JSON format.
*/
export function ImportButton() {
const [dialogOpen, setDialogOpen] = useState(false);
const [userDashboard, setUserDashboard, appList] =
useContext(ApplicationsContext);

const handleImport = (importedState: ApplicationState) => {
const states = Array.isArray(importedState)
? importedState
: [importedState];

const id: number = userDashboard.reduce(
(acc, group) =>
group.title.startsWith("Imported Applications") ? acc + 1 : acc,
0,
);

const newGroup = {
title: `Imported Applications${id > 0 ? ` (${id})` : ""}`,
extended: true,
items: [],
};

// Create a group only if there are valid states to import
if (states.some((state) => state.state !== "null"))
userDashboard.push(newGroup);

states.forEach((state) => {
if (state.state !== "null") {
const appId = handleAppCreation(
state.appType,
state.appName,
appList,
userDashboard,
setUserDashboard,
);
sessionStorage.setItem(`${appId}_State`, state.state);
} else {
console.warn(`No state to import for app type: ${state.appType}`);
}
});
};

return (
<>
<Tooltip title="Import application state">
<IconButton onClick={() => setDialogOpen(true)}>
<InputIcon />
</IconButton>
</Tooltip>

<ImportDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
onImport={handleImport}
/>
</>
);
}

/**
* Handles the creation of a new app in the dashboard drawer.
*
* @param appType - The type of the app to be created.
* @param icon - The icon component for the app.
*/
function handleAppCreation(
appType: string,
appTitle: string,
appList: ApplicationMetadata[],
userDashboard: DashboardGroup[],
setUserDashboard: React.Dispatch<SetStateAction<DashboardGroup[]>>,
): string {
const group = userDashboard[userDashboard.length - 1];

const count = userDashboard.reduce(
(sum, group) =>
sum +
group.items.filter((item) => item.title.startsWith(appTitle)).length,
0,
);

const title = count > 0 ? `${appTitle} (${count + 1})` : appTitle;
const appId = `${title}${userDashboard.reduce(
(sum, group) => sum + group.items.length,
0,
)}`;

const newApp = {
title: title,
id: appId,
type: appType,
icon: appList.find((app) => app.name === appType)?.icon || null,
};
group.items.push(newApp);
setUserDashboard((userDashboard) =>
userDashboard.map((g) => (g.title === group.title ? group : g)),
);

return appId;
}
Loading