diff --git a/Configuration/webapp/app/components/config-navigator/ConfigNavigator.tsx b/Configuration/webapp/app/components/config-navigator/ConfigNavigator.tsx new file mode 100644 index 000000000..d8bcee5c0 --- /dev/null +++ b/Configuration/webapp/app/components/config-navigator/ConfigNavigator.tsx @@ -0,0 +1,56 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { List } from '@mui/material'; +import { useEffect, useState } from 'react'; +import ConfigNavigatorItem from './ConfigNavigatorItem'; + +/** + * ConfigNavigator component + * Represents the configuration navigator sidebar. + * @returns {React.ReactElement} ConfigNavigator + */ +export const ConfigNavigator = () => { + const [configKeys, setConfigKeys] = useState([]); + const [isError, setIsError] = useState(false); + + const fetchConfigurationKeys = async () => { + try { + const res = await fetch('http://localhost:8080/control/api/configurations'); + const data = (await res.json()) as string[]; + const newConfigKeys = data?.map((key) => key.split('/').pop() ?? ''); + setConfigKeys(newConfigKeys); + } catch (_error) { + // temporarily disable the linting rule for this line + // fetching will be refactored once Tanstack Query is introduced + // eslint-disable-next-line no-console + console.error('Error while fetching configuration keys', _error); + setIsError(true); + } + }; + + useEffect(() => { + void fetchConfigurationKeys(); + }, []); + + return ( + + {isError ? ( +

Error while fetching configuration keys

+ ) : ( + configKeys?.map((text) => ) + )} +
+ ); +}; diff --git a/Configuration/webapp/app/components/config-navigator/ConfigNavigatorItem.tsx b/Configuration/webapp/app/components/config-navigator/ConfigNavigatorItem.tsx new file mode 100644 index 000000000..3a51ce89c --- /dev/null +++ b/Configuration/webapp/app/components/config-navigator/ConfigNavigatorItem.tsx @@ -0,0 +1,45 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { type FC } from 'react'; +import { ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faFile } from '@fortawesome/free-solid-svg-icons'; + +interface ConfigNavigatorItemProps { + title: string; + onClick?: () => void; +} + +/** + * ConfigNavigatorItem component + * Represents an item in the configuration navigator. + * @param {ConfigNavigatorItemProps} props - The props of the component. + * @param {string} props.title - The title of the configuration item. + * @param {Function} props.onClick - Callback function to handle item click. + * @returns {React.ReactElement} ConfigNavigatorItem + */ +const ConfigNavigatorItem: FC = ({ title, onClick }) => ( + + + + + + + + +); + +export default ConfigNavigatorItem; diff --git a/Configuration/webapp/app/components/layout/MainLayout.tsx b/Configuration/webapp/app/components/layout/MainLayout.tsx index 3be16d351..25bb05395 100644 --- a/Configuration/webapp/app/components/layout/MainLayout.tsx +++ b/Configuration/webapp/app/components/layout/MainLayout.tsx @@ -14,25 +14,21 @@ import { Box } from '@mui/material'; import { type FC, type PropsWithChildren } from 'react'; -import { LeftDrawer } from './drawer/LeftDrawer'; -import { Content } from './content/Content'; /** * MainLayout component * Represents the main layout of the application, including the left drawer and content area. * @param {PropsWithChildren} props - Component props. + * @param {ReactElement} props.children - The children elements to render inside main layout. * @returns {React.ReactElement} MainLayout */ -const MainLayout: FC = ({ children }) => ( +export const MainLayout: FC = ({ children }) => ( - - {children} + {children} ); - -export default MainLayout; diff --git a/Configuration/webapp/app/components/layout/content/ContentHeader.tsx b/Configuration/webapp/app/components/layout/content/ContentHeader.tsx index 6663379cf..8149c036c 100644 --- a/Configuration/webapp/app/components/layout/content/ContentHeader.tsx +++ b/Configuration/webapp/app/components/layout/content/ContentHeader.tsx @@ -21,11 +21,11 @@ interface ContentHeaderProps { } /** - * ContentHeader component - * Represents the header of the content area in the application layout. - * It displays the current path and includes a user section. - * @param {ContentHeaderProps} props - Component props. - * @returns {React.ReactElement} ContentHeader + * Content component + * Represents the header of the content area. + * @param {ContentHeaderProps} props - The props of the component. + * @param {string} props.currentPath - Current configuration path. + * @returns {React.ReactElement} Content */ export const ContentHeader: FC = ({ currentPath }) => ( ( +export const LeftDrawer: FC = ({ children }) => ( ( className="left-drawer" > - - - {['Item 1', 'Item 2', 'Item 3', 'Item 4'].map((text) => ( - - - - ))} - - + {children} ); diff --git a/Configuration/webapp/app/components/user-section/UserSection.tsx b/Configuration/webapp/app/components/user-section/UserSection.tsx index e129b513a..d8e972821 100644 --- a/Configuration/webapp/app/components/user-section/UserSection.tsx +++ b/Configuration/webapp/app/components/user-section/UserSection.tsx @@ -12,8 +12,8 @@ * or submit itself to any jurisdiction. */ -import { useState, type FC, type MouseEvent } from 'react'; import { Box, IconButton, Menu, MenuItem, Avatar } from '@mui/material'; +import { useState, type FC, type MouseEvent } from 'react'; import { getSessionData } from '~/services/session'; interface UserSectionProps { diff --git a/Configuration/webapp/app/root.tsx b/Configuration/webapp/app/root.tsx index 7dbfbd1c8..48324a8aa 100644 --- a/Configuration/webapp/app/root.tsx +++ b/Configuration/webapp/app/root.tsx @@ -24,10 +24,12 @@ import { import type { Route } from './+types/root'; import './app.css'; import '@aliceo2/web-ui/Frontend/css/src/bootstrap.css'; - import { Spinner } from '~/ui/spinner'; -import MainLayout from './components/layout/MainLayout'; +import { MainLayout } from './components/layout/MainLayout'; +import { LeftDrawer } from './components/layout/drawer/LeftDrawer'; +import { Content } from './components/layout/content/Content'; +import { ConfigNavigator } from './components/config-navigator/ConfigNavigator'; /** * Root component @@ -45,7 +47,12 @@ export function Layout({ children }: { children: React.ReactNode }) { - {children} + + + + + {children} + diff --git a/Configuration/webapp/package-lock.json b/Configuration/webapp/package-lock.json index cca46f8e9..736bc5219 100644 --- a/Configuration/webapp/package-lock.json +++ b/Configuration/webapp/package-lock.json @@ -9,6 +9,9 @@ "@aliceo2/web-ui": "^2.7.4", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", "@mui/material": "^7.1.1", "@react-router/node": "7.5.3", "isbot": "^5", @@ -1382,6 +1385,52 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.6.tgz", + "integrity": "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7", + "react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/Configuration/webapp/package.json b/Configuration/webapp/package.json index 087124070..fe867718b 100644 --- a/Configuration/webapp/package.json +++ b/Configuration/webapp/package.json @@ -18,6 +18,9 @@ "@aliceo2/web-ui": "^2.7.4", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", "@mui/material": "^7.1.1", "@react-router/node": "7.5.3", "isbot": "^5", diff --git a/Configuration/webapp/test/public/page-root-mocha.spec.ts b/Configuration/webapp/test/public/page-root-mocha.spec.ts index 0ea19ad80..11fbae83e 100644 --- a/Configuration/webapp/test/public/page-root-mocha.spec.ts +++ b/Configuration/webapp/test/public/page-root-mocha.spec.ts @@ -111,4 +111,40 @@ describe('`pageRoot` test-suite', function () { const userSectionMenu = await page.$$('.user-section__menu'); assert.strictEqual(userSectionMenu.length, 1); }); + + it('should successfully display configurations list', async function () { + if (page === null) { + assert.equal('Page is null', 'test suite failed'); + return; + } + + const configNavigator = await page.$$('.config_navigator'); + assert.strictEqual(configNavigator.length, 1); + }); + + it('should successfully display configurations list items', async function () { + if (page === null || url === null) { + assert.equal('Page is null', 'test suite failed'); + return; + } + + const res = await fetch('http://localhost:8080/control/api/configurations'); + const data = await res.json(); + + const configNavigatorItems = await page.$$('.config_navigator__item'); + assert.strictEqual(configNavigatorItems.length, data?.length ?? 0); + }); + + it('should display configurations list', async function () { + if (page === null || url === null) { + assert.equal('Page is null', 'test suite failed'); + return; + } + + const res = await fetch('http://localhost:8080/control/api/configurations'); + const data = await res.json(); + + const configNavigatorItems = await page.$$('.config_navigator__item'); + assert.strictEqual(configNavigatorItems.length, data?.length ?? 0); + }); });