diff --git a/portal/mock-server/src/all-experiments.json b/portal/mock-server/src/all-experiments.json new file mode 100644 index 00000000..4b9a9061 --- /dev/null +++ b/portal/mock-server/src/all-experiments.json @@ -0,0 +1,120 @@ +[ + { + "id": 15, + "name": "Experiment 1", + "end_time": 1705240065192, + "test_runs": [ + { + "id": 354, + "algorithm": "prime256v1", + "iterations": 100 + }, + { + "id": 355, + "algorithm": "prime256v1", + "iterations": 500 + }, + { + "id": 356, + "algorithm": "prime256v1", + "iterations": 1000 + }, + { + "id": 357, + "algorithm": "p256_kyber512", + "iterations": 100 + }, + { + "id": 358, + "algorithm": "p256_kyber512", + "iterations": 500 + }, + { + "id": 359, + "algorithm": "p256_kyber512", + "iterations": 1000 + }, + { + "id": 360, + "algorithm": "bikel3", + "iterations": 100 + }, + { + "id": 361, + "algorithm": "bikel3", + "iterations": 500 + }, + { + "id": 362, + "algorithm": "bikel3", + "iterations": 1000 + } + ] + }, + { + "id": 16, + "name": "Experiment 2", + "end_time": 1705389926549, + "test_runs": [ + { + "id": 363, + "algorithm": "kyber512", + "iterations": 1000 + }, + { + "id": 364, + "algorithm": "bikel3", + "iterations": 1000 + }, + { + "id": 365, + "algorithm": "prime256v1", + "iterations": 1000 + } + ] + }, + { + "id": 17, + "name": "Experiment 3", + "end_time": 1705389926549, + "test_runs": [ + { + "id": 366, + "algorithm": "prime256v1", + "iterations": 500 + }, + { + "id": 367, + "algorithm": "bikel3", + "iterations": 1000 + }, + { + "id": 368, + "algorithm": "bikel3", + "iterations": 1000 + }, + { + "id": 369, + "algorithm": "prime256v1", + "iterations": 5000 + } + ] + }, + { + "id": 18, + "name": "Experiment 4", + "end_time": 1705389926549, + "test_runs": [ + { + "id": 370, + "algorithm": "kyber512", + "iterations": 500 + }, + { + "id": 371, + "algorithm": "kyber512", + "iterations": 1500 + } + ] + } +] diff --git a/portal/mock-server/src/router.ts b/portal/mock-server/src/router.ts index b6c112c6..5db6a10d 100644 --- a/portal/mock-server/src/router.ts +++ b/portal/mock-server/src/router.ts @@ -37,6 +37,14 @@ router.get('/qujata-api/test_suites/:testSuiteId', async (req: Request, res: Res }, 1500); }); +router.get('/qujata-api/test_suites', async (req: Request, res: Response) => { + console.log(`-${req.method} ${req.url}`); + const data = (await import('./all-experiments.json')).default; + setTimeout(() => { + res.json(data); + }, 1500); +}); + router.put('/qujata-api/test_suites/:testSuiteId', async (req: Request, res: Response) => { console.log(`-${req.method} ${req.url}`); setTimeout(() => { @@ -51,4 +59,11 @@ router.delete('/qujata-api/test_suites/:testSuiteId', async (req: Request, res: }, 1500); }); +router.post('/qujata-api/test_suites/delete', async (req: Request, res: Response) => { + console.log(`-${req.method} ${req.url}`); + setTimeout(() => { + res.status(200).send(); + }, 1500); +}); + export default router; diff --git a/portal/mock-server/src/test.json b/portal/mock-server/src/test.json index 7d9ef1e9..ed4ca2d7 100644 --- a/portal/mock-server/src/test.json +++ b/portal/mock-server/src/test.json @@ -15,7 +15,7 @@ "resourceName": "RELACE_WITH_RESOURCE_NAME" }, - "testRuns": [ + "test_runs": [ { "id":1, "algorithm": "bikel1", diff --git a/portal/package.json b/portal/package.json index 45831012..025ccd6b 100644 --- a/portal/package.json +++ b/portal/package.json @@ -8,6 +8,7 @@ "chart.js": "^4.4.0", "chartjs-plugin-annotation": "^3.0.1", "classnames": "^2.3.2", + "date-fns": "^3.3.0", "lodash": "^4.17.21", "react": "^18.2.0", "react-chartjs-2": "3.1.1", diff --git a/portal/src/app/apis.ts b/portal/src/app/apis.ts index 4897274c..409568eb 100644 --- a/portal/src/app/apis.ts +++ b/portal/src/app/apis.ts @@ -1,5 +1,5 @@ const testSuites = 'test_suites'; - + export const APIS: { [key in keyof typeof API_URLS]: string } = { analyze: 'analyze', algorithms: 'algorithms', @@ -7,8 +7,10 @@ export const APIS: { [key in keyof typeof API_URLS]: string } = { testRunResults: `${testSuites}/:testSuiteId`, editExperiment: `${testSuites}/:testSuiteId`, deleteExperiment: `${testSuites}/:testSuiteId`, + allExperiments: `${testSuites}`, + deleteExperiments: `${testSuites}/delete`, }; - + enum API_URLS { analyze, algorithms, @@ -16,4 +18,6 @@ enum API_URLS { testRunResults, editExperiment, deleteExperiment, + allExperiments, + deleteExperiments } diff --git a/portal/src/app/components/all-experiments/Experiments.module.scss b/portal/src/app/components/all-experiments/Experiments.module.scss new file mode 100644 index 00000000..f60ea790 --- /dev/null +++ b/portal/src/app/components/all-experiments/Experiments.module.scss @@ -0,0 +1,67 @@ +@import "src/styles/variables-keys"; + +.experiments_wrapper { + padding-inline: 80px; + padding-block: 40px; + + .title_options_container { + display: flex; + justify-content: space-between; + align-items: center; + + .experiments_title { + font-size: 20px; + font-family: var($fontMedium); + margin-block-end: 40px; + } + + .options_wrapper { + .trash_icon { + background-color: #F5F1FF; + inline-size: 34px; + block-size: 34px; + border-radius: 50%; + } + } + + .options_wrapper:hover .hover_image { + display: block; + } + + .options_wrapper:hover .default_image { + display: none; + } + + .default_image { + padding-inline: 11px; + display: block; + } + + .hover_image { + display: none; + } + } +} + +.experiments_table { + text-align: left; + + th:first-child, + td:first-child { + text-align: center; + inline-size: 80px; + } +} + +.input_form_item { + display: none; +} + +.input_option { + margin-block-end: -5px; + + .input_option_checkbox_icon { + margin-inline-end: 10px; + cursor: pointer; + } +} diff --git a/portal/src/app/components/all-experiments/Experiments.test.tsx b/portal/src/app/components/all-experiments/Experiments.test.tsx new file mode 100644 index 00000000..6106dd28 --- /dev/null +++ b/portal/src/app/components/all-experiments/Experiments.test.tsx @@ -0,0 +1,44 @@ +import { render } from '@testing-library/react'; +import { Experiments } from './Experiments'; +import { useExperimentsData } from './hooks'; +import { FetchDataStatus, useFetch } from '../../shared/hooks/useFetch'; + +jest.mock('./hooks'); +jest.mock('../../shared/hooks/useFetch'); +jest.mock('react-router-dom', () => ({ + useNavigate: jest.fn(), +})); + +describe('Experiments', () => { + it('renders correctly', () => { + (useExperimentsData as jest.Mock).mockReturnValue({ + test_suites: [{ + id: 15, + name: "Experiment 1", + end_time: 1705240065192, + test_runs: [ + { + id: 354, + algorithm: "prime256v1", + iterations: 100 + }, + { + id: 355, + algorithm: "prime256v1", + iterations: 500 + } + ] + }], + status: FetchDataStatus.Fetching, + }); + (useFetch as jest.Mock).mockReturnValue({ + post: jest.fn(), + status: FetchDataStatus.Fetching, + error: null, + cancelRequest: jest.fn(), + }); + + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/portal/src/app/components/all-experiments/Experiments.tsx b/portal/src/app/components/all-experiments/Experiments.tsx new file mode 100644 index 00000000..024f3b52 --- /dev/null +++ b/portal/src/app/components/all-experiments/Experiments.tsx @@ -0,0 +1,170 @@ +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; +import styles from './Experiments.module.scss'; +import cn from 'classnames'; +import { IUseExperimentsData, useExperimentsData } from './hooks'; +import { FetchDataStatus, IHttp, useFetch } from '../../shared/hooks/useFetch'; +import { ALL_EXPERIMENTS_TABLE_EN } from './translate/en'; +import { CellContext } from '@tanstack/react-table'; +import { Table } from '../../shared/components/table'; +import { Button, ButtonActionType, ButtonSize, ButtonStyleType } from '../../shared/components/att-button'; +import { APIS } from '../../apis'; +import { useNavigate } from 'react-router-dom'; +import { useFetchSpinner } from '../../shared/hooks/useFetchSpinner'; +import { useErrorMessage } from '../../hooks/useErrorMessage'; +import { formatDistanceToNow } from 'date-fns'; +import CheckedSvg from '../../../assets/images/checked.svg'; +import UnCheckedSvg from '../../../assets/images/unchecked.svg'; +import TrashSvg from '../../../assets/images/trash.svg'; +import TrashHoverSvg from '../../../assets/images/trash-hover.svg'; +import DuplicateSvg from '../../../assets/images/duplicate.svg'; +import { DeleteExperimentModal } from '../home/components/experiment/components/delete-experiment-modal'; +import { parseExperimentsData } from './utils/parse-experiments-data.utils'; +import { ExperimentData } from './models/experiments.interface'; + +const DeleteAriaLabel: string = ALL_EXPERIMENTS_TABLE_EN.BUTTONS.DELETE; +const DuplicateAriaLabel: string = ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.LINKS.DUPLICATE; + +export const Experiments: React.FC = () => { + const { testSuites, status }: IUseExperimentsData = useExperimentsData(); + const [openDeleteModal, setOpenDeleteModal] = useState(false); + const [checkedRows, setCheckedRows] = useState>({}); + const experimentsData = useMemo(() => (testSuites ? parseExperimentsData(testSuites): []), [testSuites]); + const navigate = useNavigate(); + + const { post, status: deleteStatus, error: deleteError, cancelRequest: cancelRequestDelete }: IHttp + = useFetch({ url: APIS.deleteExperiments }); + useFetchSpinner(deleteStatus); + useErrorMessage(deleteError); + useEffect(() => cancelRequestDelete, [cancelRequestDelete]); + + const handleDeleteClick: () => void = useCallback((): void => { + setOpenDeleteModal(true); + }, []); + + const handleCloseDeleteExperimentModal: (confirm?: boolean) => void = useCallback((confirm?: boolean): void => { + if (confirm) { + const ids: number[] = Object.keys(checkedRows).map((key: string) => parseInt(key)) + post({ + data: { ids } + }); + } + setOpenDeleteModal(false); + }, [post, checkedRows]); + + const handleCheckboxClick = useCallback((rowInfo: ExperimentData): void => { + const rowId = rowInfo.id as number; + setCheckedRows((prevState: Record) => ({ + ...prevState, + [rowId]: !prevState[rowId], + })); + }, []); + + const handleDuplicateClick = useCallback((row: ExperimentData) => { + // Navigate to the Home Page + navigate('/qujata', { state: { row } }); + }, [navigate]); + + const headers = useMemo(() => { + const columnDefs = [ + { + id: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.CHECKBOX, + accessor: () => null, + cell: (cellInfo: CellContext) => { + const rowInfo: ExperimentData = cellInfo.row.original; + return ( +
+ row-option handleCheckboxClick(rowInfo)} + /> + handleCheckboxClick(rowInfo)} + /> +
+ ) + } + }, + { + id: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.EXPERIMENT_NAME.ID, + name: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.EXPERIMENT_NAME.NAME, + accessor: (row: ExperimentData) => row.name + }, + { + id: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.ALGORITHMS.ID, + name: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.ALGORITHMS.NAME, + accessor: (row: ExperimentData) => row.algorithms?.join(', ') + }, + { + id: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.ITERATIONS.ID, + name: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.ITERATIONS.NAME, + accessor: (row: ExperimentData) => row.iterations?.join(', ') + }, + { + id: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.DATE.ID, + name: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.DATE.NAME, + accessor: (row: ExperimentData) => formatDistanceToNow(row.end_time, { addSuffix: true }) + }, + { + id: ALL_EXPERIMENTS_TABLE_EN.TABLE_COLUMNS.LINKS.DUPLICATE, + accessor: () => null, + cell: (cellInfo: CellContext) => ( + + ) + }, + ]; + + return columnDefs.map(({ id, name, accessor, cell }) => ({ + id, + header: () => {name}, + accessor, + cell: cell || ((cellInfo: CellContext) => {cellInfo.getValue() as ReactNode}) + })); + }, [checkedRows, handleCheckboxClick, handleDuplicateClick]); + + const checkedExperimentNames = experimentsData + .filter((experiment: ExperimentData) => checkedRows[experiment.id]) + .map((experiment: ExperimentData) => experiment.name); + + return ( +
+ <> + { status === FetchDataStatus.Success && +
+ + {Object.values(checkedRows).some((value: boolean) => value) && ( + + )} +
+ } + {experimentsData.length > 0 && } + {openDeleteModal && } + + + ); +} diff --git a/portal/src/app/components/all-experiments/__snapshots__/Experiments.test.tsx.snap b/portal/src/app/components/all-experiments/__snapshots__/Experiments.test.tsx.snap new file mode 100644 index 00000000..8afcf59a --- /dev/null +++ b/portal/src/app/components/all-experiments/__snapshots__/Experiments.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Experiments renders correctly 1`] = ` +
+
+
+`; diff --git a/portal/src/app/components/all-experiments/hooks/index.ts b/portal/src/app/components/all-experiments/hooks/index.ts new file mode 100644 index 00000000..e1607221 --- /dev/null +++ b/portal/src/app/components/all-experiments/hooks/index.ts @@ -0,0 +1 @@ +export * from './useExperimentsData'; diff --git a/portal/src/app/components/all-experiments/hooks/useExperimentsData.test.ts b/portal/src/app/components/all-experiments/hooks/useExperimentsData.test.ts new file mode 100644 index 00000000..9318385d --- /dev/null +++ b/portal/src/app/components/all-experiments/hooks/useExperimentsData.test.ts @@ -0,0 +1,70 @@ +import { renderHook } from '@testing-library/react'; +import { useFetch } from '../../../shared/hooks/useFetch'; +import { Experiment } from '../models/experiments.interface'; +import { useExperimentsData } from './useExperimentsData'; + +jest.mock('../../../shared/hooks/useFetch', () => ({ + useFetch: jest.fn(), +})); +jest.mock('../../../shared/hooks/useFetchSpinner'); +jest.mock('../../../hooks/useErrorMessage'); + +describe('useExperimentsData', () => { + test('Should be in Success mode', () => { + const allExperimentsMockData: Experiment[] = [ + { + id: 17, + name: "Experiment 3", + end_time: 1705389926549, + test_runs: [ + { + id: 366, + algorithm: "prime256v1", + iterations: 500 + }, + { + id: 367, + algorithm: "bikel3", + iterations: 1000 + }, + { + id: 368, + algorithm: "p256_kyber512", + iterations: 10000 + }, + { + id: 369, + algorithm: "prime256v1", + iterations: 5000 + } + ] + }, + { + id: 18, + name: "Experiment 4", + end_time: 1705389926549, + test_runs: [ + { + id: 370, + algorithm: "kyber512", + iterations: 500 + }, + { + id: 371, + algorithm: "kyber512", + iterations: 1000 + } + ] + } + ]; + + (useFetch as jest.Mock).mockReturnValue({ + get: jest.fn(), + data: allExperimentsMockData, + cancelRequest: jest.fn(), + }); + + const { result } = renderHook(() => useExperimentsData()); + expect(result.current.testSuites.length).toEqual(allExperimentsMockData.length); + }); +}); \ No newline at end of file diff --git a/portal/src/app/components/all-experiments/hooks/useExperimentsData.ts b/portal/src/app/components/all-experiments/hooks/useExperimentsData.ts new file mode 100644 index 00000000..4e872e5e --- /dev/null +++ b/portal/src/app/components/all-experiments/hooks/useExperimentsData.ts @@ -0,0 +1,32 @@ +import { FetchDataStatus, IHttp, useFetch } from '../../../shared/hooks/useFetch'; +import { useEffect, useState } from 'react'; +import { APIS } from '../../../apis'; +import { useFetchSpinner } from '../../../shared/hooks/useFetchSpinner'; +import { useErrorMessage } from '../../../hooks/useErrorMessage'; +import { Experiment } from '../models/experiments.interface'; + +export interface IUseExperimentsData { + testSuites: Experiment[]; + status: FetchDataStatus; +} + +export function useExperimentsData(): IUseExperimentsData { + const [allExperiments, setAllExperiments] = useState([]); + const { get, data, cancelRequest, status, error }: IHttp = useFetch({ url: APIS.allExperiments }); + + useFetchSpinner(status); + useErrorMessage(error); + useEffect(() => { + get(); + return cancelRequest; + }, [get, cancelRequest]); + + + useEffect(() => { + if (data) { + setAllExperiments(data); + } + }, [data, status, allExperiments]); + + return { testSuites: allExperiments, status }; +} diff --git a/portal/src/app/components/all-experiments/models/experiments.interface.ts b/portal/src/app/components/all-experiments/models/experiments.interface.ts new file mode 100644 index 00000000..ab9f8f73 --- /dev/null +++ b/portal/src/app/components/all-experiments/models/experiments.interface.ts @@ -0,0 +1,12 @@ +import { ITestRunResult, ITestRunResultData } from '../../../shared/models/test-run-result.interface'; + +export type TestRunSubset = Pick; +export type Experiment = Pick & { test_runs: TestRunSubset[] }; + +export interface ExperimentData { + id: number; + name: string; + algorithms: string[]; + iterations: number[]; + end_time: number; +}; diff --git a/portal/src/app/components/all-experiments/translate/en.ts b/portal/src/app/components/all-experiments/translate/en.ts new file mode 100644 index 00000000..993412fa --- /dev/null +++ b/portal/src/app/components/all-experiments/translate/en.ts @@ -0,0 +1,28 @@ +export const ALL_EXPERIMENTS_TABLE_EN = { + TITLE: 'All Experiments', + TABLE_COLUMNS: { + CHECKBOX: 'checkbox', + EXPERIMENT_NAME: { + NAME: 'Experiment Name', + ID: 'experimentName' + }, + ALGORITHMS: { + NAME: 'Algorithms', + ID: 'algorithms' + }, + ITERATIONS: { + NAME: 'Iterations', + ID: 'iterations' + }, + DATE: { + NAME: 'Date', + ID: 'date' + }, + LINKS: { + DUPLICATE: 'duplicate', + } + }, + BUTTONS: { + DELETE: 'Delete', + } +} diff --git a/portal/src/app/components/all-experiments/utils/parse-experiments-data.utils.test.ts b/portal/src/app/components/all-experiments/utils/parse-experiments-data.utils.test.ts new file mode 100644 index 00000000..6695e3da --- /dev/null +++ b/portal/src/app/components/all-experiments/utils/parse-experiments-data.utils.test.ts @@ -0,0 +1,33 @@ +import { parseExperimentsData } from './parse-experiments-data.utils'; +import { ITestRunResultData } from '../../../shared/models/test-run-result.interface'; +import { Experiment, ExperimentData } from '../models/experiments.interface'; + +describe('parseExperimentsData', () => { + it('should parse experiments data correctly', () => { + const mockExperiments: Experiment[] = [ + { + id: 1, + name: 'Experiment 1', + test_runs: [ + { algorithm: 'Algorithm 1', iterations: 1000 } as ITestRunResultData, + { algorithm: 'Algorithm 2', iterations: 5000 } as ITestRunResultData, + ], + end_time: 1705240065192, + }, + ]; + + const expectedOutput: ExperimentData[] = [ + { + id: 1, + name: 'Experiment 1', + algorithms: ['Algorithm 1', 'Algorithm 2'], + iterations: [1000, 5000], + end_time: 1705240065192, + }, + ]; + + const result = parseExperimentsData(mockExperiments); + + expect(result).toEqual(expectedOutput); + }); +}); diff --git a/portal/src/app/components/all-experiments/utils/parse-experiments-data.utils.ts b/portal/src/app/components/all-experiments/utils/parse-experiments-data.utils.ts new file mode 100644 index 00000000..3ddfa6ac --- /dev/null +++ b/portal/src/app/components/all-experiments/utils/parse-experiments-data.utils.ts @@ -0,0 +1,24 @@ +import { Experiment, ExperimentData, TestRunSubset } from '../models/experiments.interface'; + +export function parseExperimentsData(test_suites: Experiment[]) { + const experimentsData: ExperimentData[] = []; + + test_suites.forEach((experiment: Experiment) => { + const algorithms = new Set(); + const iterations = new Set(); + experiment.test_runs?.forEach((testRun: TestRunSubset) => { + algorithms.add(testRun.algorithm); + iterations.add(testRun.iterations); + }); + + experimentsData.push({ + id: experiment.id, + name: experiment.name, + algorithms: Array.from(algorithms), + iterations: Array.from(iterations), + end_time: experiment.end_time + }); + }); + + return experimentsData; +} diff --git a/portal/src/app/components/home/Home.test.tsx b/portal/src/app/components/home/Home.test.tsx index e5b75f25..c76bf9f1 100644 --- a/portal/src/app/components/home/Home.test.tsx +++ b/portal/src/app/components/home/Home.test.tsx @@ -4,6 +4,7 @@ import { SubHeader, SubHeaderProps } from '../sub-header'; import { ProtocolQuery, ProtocolQueryProps } from '../protocol-query'; const mockUseNavigate = jest.fn(); +const mockUseLocation = jest.fn(); jest.mock('../sub-header'); jest.mock('../protocol-query'); @@ -17,6 +18,7 @@ jest.mock('../../hooks/useDashboardData', () => ({ jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: () => mockUseNavigate, + useLocation: () => mockUseLocation, })); describe('Home', () => { diff --git a/portal/src/app/components/home/Home.tsx b/portal/src/app/components/home/Home.tsx index ab4e551b..e05f1b2b 100644 --- a/portal/src/app/components/home/Home.tsx +++ b/portal/src/app/components/home/Home.tsx @@ -5,7 +5,8 @@ import { ProtocolQuery } from "../protocol-query"; import { SubHeader } from "../sub-header"; import { useCallback, useEffect, useState } from 'react'; import styles from './Home.module.scss'; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; +import { ExperimentData } from "../all-experiments/models/experiments.interface"; export const Home: React.FC = () => { const [isSubHeaderOpen, setIsSubHeaderOpen] = useState(true); @@ -25,6 +26,13 @@ export const Home: React.FC = () => { export const HomeContent: React.FC = () => { const { handleRunQueryClick, status, testSuiteId }: IUseDashboardData = useDashboardData(); const navigate = useNavigate(); + const location = useLocation(); + const [duplicateData, setDuplicateData] = useState(location.state?.row); + + useEffect(() => { + // Clear the state after the duplicate data has been created + setDuplicateData(undefined); + }, []); useEffect(() => { if (status === FetchDataStatus.Success && testSuiteId) { @@ -41,7 +49,12 @@ export const HomeContent: React.FC = () => { return (
- +
); }; diff --git a/portal/src/app/components/home/components/experiment/Experiment.module.scss b/portal/src/app/components/home/components/experiment/Experiment.module.scss index 28406f9f..8fd1714a 100644 --- a/portal/src/app/components/home/components/experiment/Experiment.module.scss +++ b/portal/src/app/components/home/components/experiment/Experiment.module.scss @@ -19,21 +19,3 @@ .table_options_wrapper { position: relative; } - -.spinner_wrapper { - position: sticky; - inset-block-start: 50%; - inset-inline-start: 50%; - text-align: center; -} - -.spinner_overlay { - inline-size: 100%; - block-size: 100%; - position: absolute; - background-color: #fff; - opacity: 0.6; - inset-block-start: 0; - inset-inline-start: 0; - z-index: 4; -} diff --git a/portal/src/app/components/home/components/experiment/Experiment.test.tsx b/portal/src/app/components/home/components/experiment/Experiment.test.tsx index 784fcfcd..d6c31b20 100644 --- a/portal/src/app/components/home/components/experiment/Experiment.test.tsx +++ b/portal/src/app/components/home/components/experiment/Experiment.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, waitFor, within } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { SubHeader } from './components/sub-header'; import { Charts } from './components/charts'; import { Experiment, ExperimentContent } from './Experiment'; diff --git a/portal/src/app/components/home/components/experiment/Experiment.tsx b/portal/src/app/components/home/components/experiment/Experiment.tsx index b9d36347..24b1b6f8 100644 --- a/portal/src/app/components/home/components/experiment/Experiment.tsx +++ b/portal/src/app/components/home/components/experiment/Experiment.tsx @@ -4,13 +4,10 @@ import { Charts } from './components/charts'; import { SubHeader } from './components/sub-header'; import { useExperimentData } from './components/hooks/useExperimentData'; import { ITestRunResult } from '../../../../shared/models/test-run-result.interface'; -import { FetchDataStatus } from '../../../../shared/hooks/useFetch'; -import { Spinner, SpinnerSize } from '../../../../shared/components/att-spinner'; import { useEffect, useRef, useState } from 'react'; import { EXPERIMENT_EN } from './translate/en'; import { ExperimentTabs } from './components/experiment-tabs'; import { handleSectionScrolling } from './utils'; -import { ISpinner, useSpinnerContext } from '../../../../shared/context/spinner'; import { TableOptions } from './components/table-options'; import { SelectColumnsPopup } from './components/table-options/components/select-columns-popup'; import { SelectedColumnsDefaultData, TableOptionsData } from './components/table-options/constants/table-options.const'; @@ -22,11 +19,11 @@ export type IExperimentData = { } export const Experiment: React.FC = () => { - const { data: testRunData, status } = useExperimentData(); + const { data: testRunData } = useExperimentData(); return (
- {status === FetchDataStatus.Fetching ? renderSpinner() : testRunData && } + {testRunData && }
); } @@ -36,7 +33,6 @@ export const ExperimentContent: React.FC = (props: IExperimentD const [currentSection, setCurrentSection] = useState(EXPERIMENT_EN.TABS.RESULTS_DATA); const [selectedColumns, setSelectedColumns] = useState(SelectedColumnsDefaultData); - const { isSpinnerOn }: ISpinner = useSpinnerContext(); const resultsDataRef = useRef(null); const visualizationRef = useRef(null); const tableOptionsRef = useRef(null); @@ -71,7 +67,6 @@ export const ExperimentContent: React.FC = (props: IExperimentD return ( <> - {isSpinnerOn && renderSpinner()}
@@ -99,13 +94,3 @@ export const ExperimentContent: React.FC = (props: IExperimentD ); } - -function renderSpinner() { - return ( -
-
- -
-
- ); -} diff --git a/portal/src/app/components/home/components/experiment/components/__mocks__/mocks.ts b/portal/src/app/components/home/components/experiment/components/__mocks__/mocks.ts index 04658e61..4308d519 100644 --- a/portal/src/app/components/home/components/experiment/components/__mocks__/mocks.ts +++ b/portal/src/app/components/home/components/experiment/components/__mocks__/mocks.ts @@ -5,8 +5,8 @@ export const MOCK_DATA_FOR_EXPERIMENT: ITestRunResult = { id: 1, name: "TestRun1", description: "TestRun1", - start_time: "2021-07-26T12:00:00.000Z", - end_time: "2021-07-26T12:00:00.000Z", + start_time: 1705240065192, + end_time: 1705240065192, environment_info: { resourceName: "gddn-aks", operatingSystem: "Linux", @@ -17,7 +17,7 @@ export const MOCK_DATA_FOR_EXPERIMENT: ITestRunResult = { nodeSize: "Standard_D4s_v5", codeRelease: "1.1.0", }, - testRuns: [ + test_runs: [ { id: 1, algorithm: "Algorithm1", @@ -56,8 +56,8 @@ export const MOCK_DATA_FOR_EXPERIMENT_TABLE: ExperimentTableProps = { id: 1, name: "TestRun1", description: "TestRun1", - start_time: "2021-07-26T12:00:00.000Z", - end_time: "2021-07-26T12:00:00.000Z", + start_time: 1705240065192, + end_time: 1705240065192, environment_info: { resourceName: "gddn-aks", operatingSystem: "Linux", @@ -68,7 +68,7 @@ export const MOCK_DATA_FOR_EXPERIMENT_TABLE: ExperimentTableProps = { nodeSize: "Standard_D4s_v5", codeRelease: "1.1.0", }, - testRuns: [ + test_runs: [ { id: 1, algorithm: "Algorithm1", @@ -126,8 +126,8 @@ export const MOCK_DATA_FOR_EXPERIMENT_WITH_NO_TEST_RUNS: ExperimentTableProps = id: 1, name: "TestRun1", description: "TestRun1", - start_time: "2021-07-26T12:00:00.000Z", - end_time: "2021-07-26T12:00:00.000Z", + start_time: 1705240065192, + end_time: 1705240065192, environment_info: { resourceName: "gddn-aks", operatingSystem: "Linux", @@ -138,7 +138,7 @@ export const MOCK_DATA_FOR_EXPERIMENT_WITH_NO_TEST_RUNS: ExperimentTableProps = nodeSize: "Standard_D4s_v5", codeRelease: "1.1.0", }, - testRuns: [] + test_runs: [] }, selectedColumns: [ { @@ -163,20 +163,20 @@ export const MOCK_DATA_FOR_EXPERIMENT_WITH_NO_TEST_RUNS: ExperimentTableProps = export const MOCK_SUB_HEADER: ITestRunResult = { id: 1, name: 'name', - description: 'name', - start_time: 'name', - end_time: 'name', + description: 'description', + start_time: 1705240065192, + end_time: 1705240065192, environment_info: { codeRelease: 'codeRelease', - cpu: 'codeRelease', - cpuArchitecture: 'codeRelease', - cpuClockSpeed: 'codeRelease', + cpu: 'cpu', + cpuArchitecture: 'cpuArchitecture', + cpuClockSpeed: 'cpuClockSpeed', cpuCores: 2, - nodeSize: 'codeRelease', - operatingSystem: 'codeRelease', - resourceName: 'codeRelease', + nodeSize: 'nodeSize', + operatingSystem: 'operatingSystem', + resourceName: 'resourceName', }, - testRuns: [ + test_runs: [ { id:1, algorithm: "bikel1", diff --git a/portal/src/app/components/home/components/experiment/components/charts/__mocks__/mocks.ts b/portal/src/app/components/home/components/experiment/components/charts/__mocks__/mocks.ts index 690e7966..102dd207 100644 --- a/portal/src/app/components/home/components/experiment/components/charts/__mocks__/mocks.ts +++ b/portal/src/app/components/home/components/experiment/components/charts/__mocks__/mocks.ts @@ -5,8 +5,8 @@ export const MOCK_DATA_FOR_CHARTS: IExperimentData = { id: 1, name: "TestRun1", description: "TestRun1", - start_time: "2021-07-26T12:00:00.000Z", - end_time: "2021-07-26T12:00:00.000Z", + start_time: 1705240065192, + end_time: 1705240065192, environment_info: { resourceName: "gddn-aks", operatingSystem: "Linux", @@ -17,7 +17,7 @@ export const MOCK_DATA_FOR_CHARTS: IExperimentData = { nodeSize: "Standard_D4s_v5", codeRelease: "1.1.0", }, - testRuns: [ + test_runs: [ { id: 1, algorithm: "Algorithm1", diff --git a/portal/src/app/components/home/components/experiment/components/charts/hooks/useChartsData.ts b/portal/src/app/components/home/components/experiment/components/charts/hooks/useChartsData.ts index 40475047..73eb95ed 100644 --- a/portal/src/app/components/home/components/experiment/components/charts/hooks/useChartsData.ts +++ b/portal/src/app/components/home/components/experiment/components/charts/hooks/useChartsData.ts @@ -62,8 +62,8 @@ export function useChartsData(props: IExperimentData): IUseChartsData { const [lineChartData, setLineChartData] = useState(); useEffect(() => { - if(props.data && props.data.testRuns.length > 0) { - const testRuns: ITestRunResultData[] = props.data.testRuns; + if(props.data && props.data.test_runs.length > 0) { + const testRuns: ITestRunResultData[] = props.data.test_runs; setBarChartData(testRuns); const labels: string[] = getLabels(testRuns); setBarChartLabels(labels); diff --git a/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/DeleteExperimentModal.test.tsx b/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/DeleteExperimentModal.test.tsx index 4a45342e..ada5f26b 100644 --- a/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/DeleteExperimentModal.test.tsx +++ b/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/DeleteExperimentModal.test.tsx @@ -5,7 +5,7 @@ import { DeleteExperimentModal, DeleteExperimentModalProps } from './DeleteExper describe('EditExperimentModal', () => { test('renders edit Experiment modal correctly', () => { const props: DeleteExperimentModalProps = { - name: 'Test', + name: ['Test'], onClose: jest.fn(), }; const { baseElement }: RenderResult = render(TestMe); @@ -15,7 +15,7 @@ describe('EditExperimentModal', () => { test('click submit button', () => { const handleClose = jest.fn(); const props: DeleteExperimentModalProps = { - name: 'Test', + name: ['Test'], onClose: handleClose, }; const { getByRole }: RenderResult = render(TestMe); diff --git a/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/DeleteExperimentModal.tsx b/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/DeleteExperimentModal.tsx index 70f5b935..2083c2fa 100644 --- a/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/DeleteExperimentModal.tsx +++ b/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/DeleteExperimentModal.tsx @@ -4,18 +4,16 @@ import { BaseModal } from '../../../../../../shared/components/modal'; import { ButtonActionType, ButtonSize, ButtonStyleType, IButton } from '../../../../../../shared/components/att-button'; import { DELETE_EXPERIMENT_MODAL_EN } from './translate/en'; import { BaseModalSize } from '../../../../../../shared/components/modal/base-modal.const'; -import { translateParserService } from '../../../../../../shared/utils/translate-parser'; export interface DeleteExperimentModalProps { onClose: (confirm?: boolean) => void; - name: string; + name: string[]; } export const DeleteExperimentModal: React.FC = (props: DeleteExperimentModalProps) => { const { name, onClose } = props; const [actionButtons, setActionButtons] = useState([]); - const description: string = translateParserService.interpolateString(DELETE_EXPERIMENT_MODAL_EN.DESCRIPTION, { name }); - + const experimentToDelete = name.map((experimentName, index) =>
  • {experimentName}
  • ); useLayoutEffect(() => { const submitButton: IButton = { @@ -37,7 +35,10 @@ export const DeleteExperimentModal: React.FC = (prop actionButton={actionButtons} size={BaseModalSize.SMALL} > -
    {description}
    +
    +

    {DELETE_EXPERIMENT_MODAL_EN.DESCRIPTION}

    +
      {experimentToDelete}
    +
    ); }; diff --git a/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/translate/en.ts b/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/translate/en.ts index 37e635ca..5af4202e 100644 --- a/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/translate/en.ts +++ b/portal/src/app/components/home/components/experiment/components/delete-experiment-modal/translate/en.ts @@ -1,5 +1,5 @@ export const DELETE_EXPERIMENT_MODAL_EN = { SUBMIT_ACTION: 'Confirm', TITLE: 'Delete Experiment', - DESCRIPTION: 'Are you sure you want to delete "{{name}}" experiment?', + DESCRIPTION: 'Are you sure you want to delete the following experiment(s)?' }; diff --git a/portal/src/app/components/home/components/experiment/components/experiment-table/ExperimentTable.module.scss b/portal/src/app/components/home/components/experiment/components/experiment-table/ExperimentTable.module.scss index 19614b4a..f474ca8c 100644 --- a/portal/src/app/components/home/components/experiment/components/experiment-table/ExperimentTable.module.scss +++ b/portal/src/app/components/home/components/experiment/components/experiment-table/ExperimentTable.module.scss @@ -1,5 +1,4 @@ @import "src/styles/variables-keys"; -@import "src/styles/z-index"; .experiment_table_wrapper { font-size: 14px; @@ -8,3 +7,12 @@ display: flex; flex-wrap: wrap; } + +.experiment_table { + text-align: center; + + th:first-child, + td:first-child { + inline-size: 80px; + } +} \ No newline at end of file diff --git a/portal/src/app/components/home/components/experiment/components/experiment-table/ExperimentTable.tsx b/portal/src/app/components/home/components/experiment/components/experiment-table/ExperimentTable.tsx index d468223b..49fa4dc2 100644 --- a/portal/src/app/components/home/components/experiment/components/experiment-table/ExperimentTable.tsx +++ b/portal/src/app/components/home/components/experiment/components/experiment-table/ExperimentTable.tsx @@ -12,9 +12,9 @@ export interface ExperimentTableProps { } export const ExperimentTable: React.FC = (props: ExperimentTableProps) => { - const data = useMemo(() => (props.data ? props.data.testRuns : []), [props.data]); + const data = useMemo(() => (props.data ? props.data.test_runs : []), [props.data]); - const headers: TableColumn[] = useMemo(() => [ + const headers: TableColumn[] = useMemo(() => [ { id: 'hashtag', header: () => {EXPERIMENT_TABLE_EN.TABLE_TITLES.HASHTAG}, @@ -44,7 +44,7 @@ export const ExperimentTable: React.FC = (props: Experimen return (
    -
    +
    ); }; diff --git a/portal/src/app/components/home/components/experiment/components/hooks/useExperimentData.test.ts b/portal/src/app/components/home/components/experiment/components/hooks/useExperimentData.test.ts index c59d722d..38a54179 100644 --- a/portal/src/app/components/home/components/experiment/components/hooks/useExperimentData.test.ts +++ b/portal/src/app/components/home/components/experiment/components/hooks/useExperimentData.test.ts @@ -21,10 +21,10 @@ describe('useExperimentData', () => { (useFetchSpinner as jest.Mock).mockImplementation(() => undefined); (useErrorMessage as jest.Mock).mockImplementation(() => undefined); - const mockDataNumOfTestRuns = MOCK_DATA_FOR_EXPERIMENT.testRuns.length; + const mockDataNumOfTestRuns = MOCK_DATA_FOR_EXPERIMENT.test_runs.length; const { result } = renderHook(() => useExperimentData()); - expect(result.current.data.testRuns.length).toEqual(mockDataNumOfTestRuns); + expect(result.current.data.test_runs.length).toEqual(mockDataNumOfTestRuns); }); test('Should not render data', () => { diff --git a/portal/src/app/components/home/components/experiment/components/hooks/useExperimentData.ts b/portal/src/app/components/home/components/experiment/components/hooks/useExperimentData.ts index 6039ed60..1c0898a7 100644 --- a/portal/src/app/components/home/components/experiment/components/hooks/useExperimentData.ts +++ b/portal/src/app/components/home/components/experiment/components/hooks/useExperimentData.ts @@ -4,14 +4,13 @@ import { replaceParams } from "../../../../../../shared/utils/replaceParams"; import { useParams } from "react-router-dom"; import { ITestRunResult, ITestRunResultData } from "../../../../../../shared/models/test-run-result.interface"; import { TestRunUrlParams } from "../../../../../../shared/models/url-params.interface"; -import { FetchDataStatus, IHttp, useFetch } from "../../../../../../shared/hooks/useFetch"; +import { IHttp, useFetch } from "../../../../../../shared/hooks/useFetch"; import { sortDataByAlgorithm } from "../charts/utils/test-run.utils"; import { useFetchSpinner } from "../../../../../../shared/hooks/useFetchSpinner"; import { useErrorMessage } from "../../../../../../hooks/useErrorMessage"; export interface IUseExperimentData { data: ITestRunResult; - status: FetchDataStatus; } export function useExperimentData(): IUseExperimentData { @@ -29,14 +28,13 @@ export function useExperimentData(): IUseExperimentData { }, [get, cancelRequest]); useEffect(() => { - if (data && data.testRuns) { - const sortedData: ITestRunResultData[] = sortDataByAlgorithm(data.testRuns); - setTestRunData({ ...data, testRuns: sortedData }); + if (data && data.test_runs) { + const sortedData: ITestRunResultData[] = sortDataByAlgorithm(data.test_runs); + setTestRunData({ ...data, test_runs: sortedData }); } }, [data]); return { - data: testRunData, - status, + data: testRunData } as IUseExperimentData; } diff --git a/portal/src/app/components/home/components/experiment/components/sub-header/SubHeader.tsx b/portal/src/app/components/home/components/experiment/components/sub-header/SubHeader.tsx index 6818c4f3..6c884ace 100644 --- a/portal/src/app/components/home/components/experiment/components/sub-header/SubHeader.tsx +++ b/portal/src/app/components/home/components/experiment/components/sub-header/SubHeader.tsx @@ -62,8 +62,8 @@ export const SubHeader: React.FC = (props: SubHeaderProps) => { const handleDownloadClick: () => void = useCallback((): void => { const csvFileName: string = `${SUB_HEADER_EN.CSV_REPORT.FILE_NAME}-${name || ''}.csv`; - downloadCsvFile(mapExperimentDataToCsvDataType(data.testRuns), csvFileName); - }, [data.testRuns, name]); + downloadCsvFile(mapExperimentDataToCsvDataType(data.test_runs), csvFileName); + }, [data.test_runs, name]); const handleCloseEditExperimentModal: (editData?: EditExperimentModalData) => void = useCallback((editData?: EditExperimentModalData): void => { if (editData) { @@ -103,11 +103,11 @@ export const SubHeader: React.FC = (props: SubHeaderProps) => {
    {SUB_HEADER_EN.ALGORITHM}
    -
    {getAlgorithmsName(data.testRuns)}
    +
    {getAlgorithmsName(data.test_runs)}
    {SUB_HEADER_EN.ITERATIONS}
    -
    {getIterations(data.testRuns)}
    +
    {getIterations(data.test_runs)}
    {experimentDescription}
    @@ -136,7 +136,7 @@ export const SubHeader: React.FC = (props: SubHeaderProps) => { {openEditModal && } - {openDeleteModal && } + {openDeleteModal && } ); } diff --git a/portal/src/app/components/home/components/experiment/components/table-options/__snapshots__/TableOptions.test.tsx.snap b/portal/src/app/components/home/components/experiment/components/table-options/__snapshots__/TableOptions.test.tsx.snap index 7ebde19e..da190153 100644 --- a/portal/src/app/components/home/components/experiment/components/table-options/__snapshots__/TableOptions.test.tsx.snap +++ b/portal/src/app/components/home/components/experiment/components/table-options/__snapshots__/TableOptions.test.tsx.snap @@ -6,7 +6,7 @@ exports[`TableOptions renders without crashing 1`] = ` > diff --git a/portal/src/app/components/protocol-query/ProtocolQuery.test.tsx b/portal/src/app/components/protocol-query/ProtocolQuery.test.tsx index c599c9fc..c444fa76 100644 --- a/portal/src/app/components/protocol-query/ProtocolQuery.test.tsx +++ b/portal/src/app/components/protocol-query/ProtocolQuery.test.tsx @@ -6,12 +6,10 @@ import { PROTOCOL_QUERY_EN } from './translate/en'; describe('ProtocolQuery', () => { let props: ProtocolQueryProps; beforeAll(() => { - // Prepare the props for the ProtocolQuery component props = { isFetching: false, - canExportFile: true, onRunClick: jest.fn(), - onDownloadDataClicked: jest.fn(), + setDuplicateData: jest.fn() }; }); diff --git a/portal/src/app/components/protocol-query/ProtocolQuery.tsx b/portal/src/app/components/protocol-query/ProtocolQuery.tsx index 0e87ab5a..c4865ea5 100644 --- a/portal/src/app/components/protocol-query/ProtocolQuery.tsx +++ b/portal/src/app/components/protocol-query/ProtocolQuery.tsx @@ -10,6 +10,8 @@ import { Spinner, SpinnerSize } from '../../shared/components/att-spinner'; import { useGetAlgorithms, useGetIterations } from './hooks'; import { handleAlgorithmsSelection } from './utils'; import { AlgorithmsSelectorCustomOption, IterationsSelectorCustomOption } from '../../shared/components/selector-custom-option'; +import { ExperimentData } from '../all-experiments/models/experiments.interface'; +import { useDuplicateData } from './hooks'; export type SelectOptionType = AttSelectOption | Options | null; type onTextChangedEvent = (e: React.ChangeEvent) => void; @@ -18,13 +20,13 @@ export type OnSelectChanged = (event: SelectOptionType) => void; export interface ProtocolQueryProps { isFetching: boolean; - canExportFile?: boolean; onRunClick: (data: ITestParams) => void; - onDownloadDataClicked?: () => void; + duplicateData?: ExperimentData; + setDuplicateData: (data?: ExperimentData) => void; } export const ProtocolQuery: React.FC = (props: ProtocolQueryProps) => { - const { isFetching, canExportFile, onRunClick, onDownloadDataClicked } = props; + const { isFetching, onRunClick, duplicateData, setDuplicateData } = props; const { algorithmOptions, algosBySection } = useGetAlgorithms(); const { iterationsOptions } = useGetIterations(); @@ -37,7 +39,9 @@ export const ProtocolQuery: React.FC = (props: ProtocolQuery const [showInputOption, setShowInputOption] = useState(false); const [inputValue, setInputValue] = useState(''); const [iterationsMenuIsOpen, setIterationsMenuIsOpen] = useState(false); - + + useDuplicateData({ data: duplicateData, setDuplicateData, setExperimentName, setAlgorithms, setIterationsCount }); + const onSubmitHandler = (event: React.FormEvent) => { event.preventDefault(); onRunClick({ @@ -86,6 +90,7 @@ export const ProtocolQuery: React.FC = (props: ProtocolQuery = (props: ProtocolQuery } - {/* */} ); }; diff --git a/portal/src/app/components/protocol-query/__snapshots__/ProtocolQuery.test.tsx.snap b/portal/src/app/components/protocol-query/__snapshots__/ProtocolQuery.test.tsx.snap index f4846208..73a8f54e 100644 --- a/portal/src/app/components/protocol-query/__snapshots__/ProtocolQuery.test.tsx.snap +++ b/portal/src/app/components/protocol-query/__snapshots__/ProtocolQuery.test.tsx.snap @@ -45,6 +45,7 @@ exports[`ProtocolQuery should render ProtocolQuery 1`] = ` class="input_form_item" placeholder="" required="" + value="" />
    { + it('should set experiment name, algorithms, and iterations count when duplicate data is provided', () => { + const setExperimentName = jest.fn(); + const setAlgorithms = jest.fn(); + const setIterationsCount = jest.fn(); + const setDuplicateData = jest.fn(); + + const duplicateData: ExperimentData = { + id: 1111, + name: 'test', + algorithms: ['algorithm1', 'algorithm2'], + iterations: [1, 2, 3], + end_time: 1705240065192, + }; + + const { rerender } = renderHook((props: DuplicateData) => useDuplicateData(props), { + initialProps: { + data: undefined, + setDuplicateData, + setExperimentName, + setAlgorithms, + setIterationsCount, + } as DuplicateData, + }); + + expect(setExperimentName).not.toHaveBeenCalled(); + expect(setAlgorithms).not.toHaveBeenCalled(); + expect(setIterationsCount).not.toHaveBeenCalled(); + expect(setDuplicateData).not.toHaveBeenCalled(); + + rerender({ + data: duplicateData, + setDuplicateData, + setExperimentName, + setAlgorithms, + setIterationsCount, + }); + + expect(setExperimentName).toHaveBeenCalledWith(duplicateData.name); + expect(setAlgorithms).toHaveBeenCalledWith(duplicateData.algorithms.map(algorithm => ({ label: algorithm, value: algorithm } as AttSelectOption))); + expect(setIterationsCount).toHaveBeenCalledWith(duplicateData.iterations.map(iteration => ({ label: iteration.toString(), value: iteration.toString() } as AttSelectOption))); + expect(setDuplicateData).toHaveBeenCalledWith(undefined); + }); +}); diff --git a/portal/src/app/components/protocol-query/hooks/useDuplicateData.ts b/portal/src/app/components/protocol-query/hooks/useDuplicateData.ts new file mode 100644 index 00000000..8cf59d88 --- /dev/null +++ b/portal/src/app/components/protocol-query/hooks/useDuplicateData.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; +import { AttSelectOption } from '../../../shared/components/att-select'; +import { ExperimentData } from '../../all-experiments/models/experiments.interface'; + +export type DuplicateData = { + data: ExperimentData | undefined, + setDuplicateData: (data: any) => void, + setExperimentName: (name: string) => void, + setAlgorithms: (options: AttSelectOption[]) => void, + setIterationsCount: (options: AttSelectOption[]) => void +} +export const useDuplicateData = (duplicate: DuplicateData) => { + useEffect(() => { + if (duplicate.data) { + const duplicateData = duplicate.data; + if (duplicateData.name) { + duplicate.setExperimentName(duplicateData.name); + } + if (duplicateData.algorithms) { + const algorithmOptions = duplicateData.algorithms.map((algorithm: string) => { + return { label: algorithm, value: algorithm } as AttSelectOption; + }); + duplicate.setAlgorithms(algorithmOptions); + } + + if (duplicateData.iterations) { + const iterationsOptions = duplicateData.iterations.map((iteration: number) => { + return { label: iteration.toString(), value: iteration.toString() } as AttSelectOption; + }); + duplicate.setIterationsCount(iterationsOptions); + } + duplicate.setDuplicateData(undefined); + } + }, [duplicate]); +}; diff --git a/portal/src/app/components/sub-header/SubHeader.test.tsx b/portal/src/app/components/sub-header/SubHeader.test.tsx index 5ee886f9..08570f2b 100644 --- a/portal/src/app/components/sub-header/SubHeader.test.tsx +++ b/portal/src/app/components/sub-header/SubHeader.test.tsx @@ -10,6 +10,6 @@ describe('SubHeader', () => { (Button as jest.Mock).mockImplementation(() =>
    Button
    ); const { container, getByText }: RenderResult = render(, { wrapper: MemoryRouter }); expect(container.firstChild).toMatchSnapshot(); - expect(getByText('That’s what are we doing on each iteration:')).toBeTruthy(); + expect(getByText('That’s what we are doing on each iteration:')).toBeTruthy(); }); }); \ No newline at end of file diff --git a/portal/src/app/components/sub-header/__snapshots__/SubHeader.test.tsx.snap b/portal/src/app/components/sub-header/__snapshots__/SubHeader.test.tsx.snap index f1d10960..23777f36 100644 --- a/portal/src/app/components/sub-header/__snapshots__/SubHeader.test.tsx.snap +++ b/portal/src/app/components/sub-header/__snapshots__/SubHeader.test.tsx.snap @@ -10,7 +10,7 @@ exports[`SubHeader renders sub header 1`] = `
    - That’s what are we doing on each iteration: + That’s what we are doing on each iteration:
    { id: string; - header: (context: HeaderContext) => React.ReactNode; - accessor: (row: ITestRunResultData) => any; - cell?: (cellInfo: CellContext) => JSX.Element; + header: (context: HeaderContext) => React.ReactNode; + accessor: (row: T) => any; + cell?: (cellInfo: CellContext, row?: T) => JSX.Element; } -export interface TableProps { - headers: TableColumn[]; - data: ITestRunResultData[]; +export interface TableProps { + className?: string; + headers: TableColumn[]; + data: T[]; } -export const Table: React.FC = ({ headers, data }) => { +export const Table = ({ headers, data, className }: TableProps) => { const [sorting, setSorting] = useState([]) - const columns: ColumnDef[] = useMemo(() => { + const columns: ColumnDef[] = useMemo(() => { return headers.map(header => ({ id: header.id, header: header.header, @@ -56,11 +57,11 @@ export const Table: React.FC = ({ headers, data }) => { }); return ( -
    +
    - {table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( + {table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( - {headerGroup.headers.map((header: Header) => ( + {headerGroup.headers.map((header: Header) => ( - {table.getRowModel().rows.map((row: Row) => ( + {table.getRowModel().rows.map((row: Row) => ( - {row.getVisibleCells().map((cell: Cell) => ( - ))} diff --git a/portal/src/app/shared/constants/navigation-tabs.const.ts b/portal/src/app/shared/constants/navigation-tabs.const.ts index 12ff222a..0a3d00e2 100644 --- a/portal/src/app/shared/constants/navigation-tabs.const.ts +++ b/portal/src/app/shared/constants/navigation-tabs.const.ts @@ -5,9 +5,8 @@ export const tabs = [ link: '/qujata', title: SHARED_EN.NAVIGATION_TABS.HOME, }, - // { - // link: '/all-experiments', - // title: SHARED_EN.NAVIGATION_TABS.ALL_EXPERIMENTS, - // disabled: true, - // } + { + link: '/qujata/test_suites', + title: SHARED_EN.NAVIGATION_TABS.ALL_EXPERIMENTS, + } ]; diff --git a/portal/src/app/shared/models/test-run-result.interface.ts b/portal/src/app/shared/models/test-run-result.interface.ts index 809f3bd6..2f451f4b 100644 --- a/portal/src/app/shared/models/test-run-result.interface.ts +++ b/portal/src/app/shared/models/test-run-result.interface.ts @@ -24,8 +24,8 @@ export interface ITestRunResult { id: number; name: string; description: string; - start_time: string; - end_time: string; + start_time: number; + end_time: number; environment_info: IEnvironmentInfo; - testRuns: ITestRunResultData[]; + test_runs: ITestRunResultData[]; } diff --git a/portal/src/assets/images/duplicate.svg b/portal/src/assets/images/duplicate.svg new file mode 100644 index 00000000..214d3d93 --- /dev/null +++ b/portal/src/assets/images/duplicate.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/portal/src/assets/images/trash-hover.svg b/portal/src/assets/images/trash-hover.svg new file mode 100644 index 00000000..81fe1736 --- /dev/null +++ b/portal/src/assets/images/trash-hover.svg @@ -0,0 +1,4 @@ + + + + diff --git a/portal/src/routes/Root.jsx b/portal/src/routes/Root.jsx index a4964046..c6bf76ca 100644 --- a/portal/src/routes/Root.jsx +++ b/portal/src/routes/Root.jsx @@ -1,12 +1,27 @@ +import styles from './Root.module.scss'; import { GlobalHeader } from '../app/shared/components/global-header/index'; import { Outlet } from 'react-router-dom'; import { tabs } from '../app/shared/constants/navigation-tabs.const'; +import { Spinner, SpinnerSize } from '../app/shared/components/att-spinner'; +import { useSpinnerContext } from '../app/shared/context/spinner'; export default function Root() { + const { isSpinnerOn } = useSpinnerContext(); return ( <> + {isSpinnerOn && renderSpinner()} ); } + +function renderSpinner() { + return ( +
    +
    + +
    +
    + ); +} diff --git a/portal/src/routes/Root.module.scss b/portal/src/routes/Root.module.scss new file mode 100644 index 00000000..a08a79d2 --- /dev/null +++ b/portal/src/routes/Root.module.scss @@ -0,0 +1,19 @@ +@import "src/styles/variables-keys"; + +.spinner_wrapper { + position: sticky; + inset-block-start: 50%; + inset-inline-start: 50%; + text-align: center; +} + +.spinner_overlay { + inline-size: 100%; + block-size: 100%; + position: absolute; + background-color: var($primaryWhite); + opacity: 0.6; + inset-block-start: 0; + inset-inline-start: 0; + z-index: 4; +} diff --git a/portal/src/routes/index.jsx b/portal/src/routes/index.jsx index a84f73d0..c9dabd47 100644 --- a/portal/src/routes/index.jsx +++ b/portal/src/routes/index.jsx @@ -2,26 +2,26 @@ import { createBrowserRouter } from 'react-router-dom'; import Root from './Root'; import { Home } from '../app/components/home/Home'; import { Experiment } from '../app/components/home/components/experiment/Experiment'; +import { Experiments } from '../app/components/all-experiments/Experiments'; -const isAllExperimentTabEnabled = false; export const router = createBrowserRouter([ { - path: '/qujata', - element: , - children: [ - { - path: '', - index: true, - element: , - }, - { - path: 'experiment/:testSuiteId', - element: , - }, - ...(isAllExperimentTabEnabled ? [{ - path: 'All-Experiments', - element:
    All Experiments
    , - }] : []), - ], + path: '/qujata', + element: , + children: [ + { + path: '', + index: true, + element: , + }, + { + path: 'experiment/:testSuiteId', + element: , + }, + { + path: 'test_suites', + element: , + }, + ], }, ]); diff --git a/portal/yarn.lock b/portal/yarn.lock index ddc70411..fcf80b7b 100644 --- a/portal/yarn.lock +++ b/portal/yarn.lock @@ -4034,6 +4034,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +date-fns@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.3.0.tgz#c1681691cf751a1d371279099a45e71409c7c761" + integrity sha512-xuouT0GuI2W8yXhCMn/AXbSl1Av3wu2hJXxMnnILTY3bYY0UgNK0qOwVXqdFBrobW5qbX1TuOTgMw7c2H2OuhA== + debug@2.6.9, debug@^2.6.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
    = ({ headers, data }) => { ))}
    + {row.getVisibleCells().map((cell: Cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())}