From 0fe29e24d475b4854684dc120ca51addce9714be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Weso=C5=82owski?= Date: Wed, 4 Feb 2026 14:46:19 +0100 Subject: [PATCH 1/6] fix issue with missing key --- .../ui/src/features/annotator/labels/labels.component.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/application/ui/src/features/annotator/labels/labels.component.tsx b/application/ui/src/features/annotator/labels/labels.component.tsx index 04f7c6eecd..890f0f58a9 100644 --- a/application/ui/src/features/annotator/labels/labels.component.tsx +++ b/application/ui/src/features/annotator/labels/labels.component.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: Apache-2.0 -import { CSSProperties } from 'react'; +import { CSSProperties, Fragment } from 'react'; import { Divider, Flex, Text } from '@geti/ui'; import { clsx } from 'clsx'; @@ -167,16 +167,15 @@ export const Labels = ({ isClassification = false, isMultiLabel = false, isReadO aria-disabled={isReadOnly} > {labels.map((label) => ( - <> + {label.id === EMPTY_LABEL_ID && } handleLabelClick(label)} /> - + ))} From 8139203871da2ecf4a0e037def543a60373c0d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Weso=C5=82owski?= Date: Thu, 5 Feb 2026 11:16:59 +0100 Subject: [PATCH 2/6] feat: add disabled model state and indicator --- .../group-models-container.component.tsx | 2 ++ .../model-actions/model-actions.component.tsx | 6 ++++-- .../model-row/model-row.component.test.tsx | 12 ++++++++++++ .../components/model-row/model-row.component.tsx | 9 +++++++-- .../src/features/models/model-listing/utils/utils.ts | 6 ++++++ 5 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 application/ui/src/features/models/model-listing/utils/utils.ts diff --git a/application/ui/src/features/models/model-listing/components/group-models-container/group-models-container.component.tsx b/application/ui/src/features/models/model-listing/components/group-models-container/group-models-container.component.tsx index f3b279e04a..7e03640a69 100644 --- a/application/ui/src/features/models/model-listing/components/group-models-container/group-models-container.component.tsx +++ b/application/ui/src/features/models/model-listing/components/group-models-container/group-models-container.component.tsx @@ -7,6 +7,7 @@ import type { Model } from '../../../../../constants/shared-types'; import { ModelDetailsTabs } from '../../model-details/model-details-tabs.component'; import { useModelListing } from '../../provider/model-listing-provider'; import { ArchitectureGroup, DatasetGroup } from '../../types'; +import { isFailedModel } from '../../utils/utils'; import { GroupHeader } from '../group-headers/group-header.component'; import { ModelRowContainer } from '../model-row/model-row.container'; import { ModelsTableHeader } from '../models-table-header.component'; @@ -35,6 +36,7 @@ export const GroupModelsContainer = ({ group, models }: GroupModelsContainerProp isQuiet UNSAFE_className={classes.disclosure} isExpanded={expandedModelIds.has(modelId)} + isDisabled={isFailedModel(model)} onExpandedChange={() => onExpandModel(modelId)} data-testid={`model-disclosure-${modelId}`} > diff --git a/application/ui/src/features/models/model-listing/components/model-actions/model-actions.component.tsx b/application/ui/src/features/models/model-listing/components/model-actions/model-actions.component.tsx index e40b5e6d08..b0ac25e554 100644 --- a/application/ui/src/features/models/model-listing/components/model-actions/model-actions.component.tsx +++ b/application/ui/src/features/models/model-listing/components/model-actions/model-actions.component.tsx @@ -11,13 +11,13 @@ import type { Model } from '../../../../../constants/shared-types'; import { usePatchPipeline } from '../../../../../hooks/api/pipeline.hook'; import { useDeleteModel } from '../../../hooks/api/use-delete-model.hook'; import { useRenameModel } from '../../../hooks/api/use-rename-model.hook'; +import { isFailedModel } from '../../utils/utils'; import { RenameModelDialog } from '../model-row/rename-model-dialog.component'; const MODEL_ACTIONS = { ACTIVE: 'active', RENAME: 'rename', DELETE: 'delete', - EXPORT: 'export', }; type ModelActionsProps = { @@ -33,6 +33,8 @@ export const ModelActions = ({ model }: ModelActionsProps) => { const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const disabled_keys = isFailedModel(model) ? [MODEL_ACTIONS.ACTIVE, MODEL_ACTIONS.RENAME] : []; + const handleAction = (key: Key) => { if (key === MODEL_ACTIONS.ACTIVE) { patchPipelineMutation.mutate({ @@ -70,7 +72,7 @@ export const ModelActions = ({ model }: ModelActionsProps) => { - + Set as active Rename Delete diff --git a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.test.tsx b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.test.tsx index bee54e6e84..4bc82b5fb6 100644 --- a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.test.tsx +++ b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.test.tsx @@ -49,6 +49,18 @@ describe('ModelRow', () => { expect(screen.getByText('-')).toBeInTheDocument(); }); + + it('renders "Failed" badge when training status is failed', () => { + const failedModel = getMockedModel({ + training_info: { + status: 'failed', + }, + }); + + render(); + + expect(screen.getByText('Failed')).toBeInTheDocument(); + }); }); describe('active model tag', () => { diff --git a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx index 86df454579..0fb4faccd5 100644 --- a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx +++ b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: Apache-2.0 -import { Flex, Grid, Tag, Text } from '@geti/ui'; +import { Badge, Flex, Grid, Tag, Text } from '@geti/ui'; import { ReactComponent as ThumbsUp } from '../../../../../assets/icons/thumbs-up.svg'; import type { Model } from '../../../../../constants/shared-types'; @@ -9,6 +9,7 @@ import { GRID_COLUMNS } from '../../constants'; import { AccuracyIndicator } from '../../model-variants/accuracy-indicator.component'; import { formatTrainingDateTime } from '../../utils/date-formatting'; import { formatModelSize } from '../../utils/format-model-size'; +import { isFailedModel } from '../../utils/utils'; import { ActiveModelTag } from '../active-model-tag.component'; import { ParentRevisionModel } from '../parent-revision-model.component'; @@ -21,6 +22,10 @@ type ModelRowProps = { onExpandModel?: (modelId: string) => void; }; +const FailedModel = () => { + return Failed; +}; + export const ModelRow = ({ model, activeModelArchitectureId, parentRevisionModel, onExpandModel }: ModelRowProps) => { const trainingEndTime = model.training_info.end_time; const totalSize = model.size; @@ -30,7 +35,7 @@ export const ModelRow = ({ model, activeModelArchitectureId, parentRevisionModel - {model.name ?? 'Unnamed Model'} + {model.name ?? 'Unnamed Model'} {isFailedModel(model) && } {model.id === activeModelArchitectureId && } diff --git a/application/ui/src/features/models/model-listing/utils/utils.ts b/application/ui/src/features/models/model-listing/utils/utils.ts new file mode 100644 index 0000000000..587b975287 --- /dev/null +++ b/application/ui/src/features/models/model-listing/utils/utils.ts @@ -0,0 +1,6 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { Model } from '../../../../constants/shared-types'; + +export const isFailedModel = (model: Pick): boolean => model.training_info?.status === 'failed'; From b4d0f2e9291a008c131426191dfb4fb17f37b6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Weso=C5=82owski?= Date: Thu, 5 Feb 2026 15:52:46 +0100 Subject: [PATCH 3/6] fix: display architecture info when grouped by dataset, display dataset info when grouped by architecture --- .../dataset-group-header.component.tsx | 15 +-- .../group-headers/group-headers.module.scss | 12 --- .../architecture-column.component.tsx | 26 +++++ .../dataset-revision-column.component.tsx | 54 ++++++++++ .../model-row/model-badge.component.tsx | 25 +++++ .../model-row/model-row.component.test.tsx | 82 +++++++++++--- .../model-row/model-row.component.tsx | 34 ++++-- .../model-row/model-row.container.tsx | 5 +- .../model-row/model-row.module.scss | 20 +++- .../current-model-training.component.tsx | 16 ++- .../current-model-training.module.scss | 6 -- .../training-model-row.component.test.tsx | 101 +++++++++++++++--- .../training-model-row.component.tsx | 84 +++++++++++---- .../model-listing/model-listing.container.tsx | 6 +- .../provider/model-listing-provider.tsx | 3 + .../model-listing/utils/date-formatting.ts | 4 + 16 files changed, 400 insertions(+), 93 deletions(-) delete mode 100644 application/ui/src/features/models/model-listing/components/group-headers/group-headers.module.scss create mode 100644 application/ui/src/features/models/model-listing/components/model-row/architecture-column.component.tsx create mode 100644 application/ui/src/features/models/model-listing/components/model-row/dataset-revision-column.component.tsx create mode 100644 application/ui/src/features/models/model-listing/components/model-row/model-badge.component.tsx diff --git a/application/ui/src/features/models/model-listing/components/group-headers/dataset-group-header.component.tsx b/application/ui/src/features/models/model-listing/components/group-headers/dataset-group-header.component.tsx index f8b33092cc..a8f4756344 100644 --- a/application/ui/src/features/models/model-listing/components/group-headers/dataset-group-header.component.tsx +++ b/application/ui/src/features/models/model-listing/components/group-headers/dataset-group-header.component.tsx @@ -3,14 +3,14 @@ import { dimensionValue, Flex, Grid, Heading, Text } from '@geti/ui'; import { Image, Tag } from '@geti/ui/icons'; +import { useNumberFormatter } from 'react-aria'; import { TrainModel } from '../../../train-model/train-model.component'; import type { DatasetGroup } from '../../types'; import { DatasetActions } from '../dataset-actions/dataset-actions.component'; +import { ModelBadge } from '../model-row/model-badge.component'; import { ThreeSectionRange } from '../three-section-range/three-section-range.component'; -import classes from './group-headers.module.scss'; - type DatasetGroupHeaderProps = { dataset: DatasetGroup; }; @@ -20,6 +20,7 @@ export const DatasetGroupHeader = ({ dataset }: DatasetGroupHeaderProps) => { const gridColumns = hasDatasetRevisionData ? ['auto', '1fr', 'auto', '1fr', 'auto'] : ['auto', '1fr', 'auto', 'auto']; + const formatter = useNumberFormatter(); return ( @@ -39,12 +40,12 @@ export const DatasetGroupHeader = ({ dataset }: DatasetGroupHeaderProps) => { - + {dataset.labelCount} - - - {dataset.imageCount.toLocaleString()} - + + + {formatter.format(dataset.imageCount)} + {hasDatasetRevisionData && ( diff --git a/application/ui/src/features/models/model-listing/components/group-headers/group-headers.module.scss b/application/ui/src/features/models/model-listing/components/group-headers/group-headers.module.scss deleted file mode 100644 index c84d594c6a..0000000000 --- a/application/ui/src/features/models/model-listing/components/group-headers/group-headers.module.scss +++ /dev/null @@ -1,12 +0,0 @@ -.tag { - background-color: var(--spectrum-gray-100); - border-radius: var(--spectrum-global-dimension-size-50); - padding: var(--spectrum-global-dimension-size-25) var(--spectrum-global-dimension-size-50); - align-items: center; - justify-content: center; - color: var(--spectrum-global-color-gray-800); - - svg { - fill: var(--spectrum-global-color-gray-800); - } -} diff --git a/application/ui/src/features/models/model-listing/components/model-row/architecture-column.component.tsx b/application/ui/src/features/models/model-listing/components/model-row/architecture-column.component.tsx new file mode 100644 index 0000000000..26bde125e6 --- /dev/null +++ b/application/ui/src/features/models/model-listing/components/model-row/architecture-column.component.tsx @@ -0,0 +1,26 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { Flex, Text } from '@geti/ui'; + +import { ReactComponent as ThumbsUp } from '../../../../../assets/icons/thumbs-up.svg'; +import { ModelBadge } from './model-badge.component'; + +import styles from './model-row.module.scss'; + +type ArchitectureColumnProps = { + architecture: string; +}; + +export const ArchitectureColumn = ({ architecture }: ArchitectureColumnProps) => { + return ( + + {architecture} (Apache 2.0) + {/* TODO: Speed is hardcoded for now, once the backend is update we need to update this */} + + + Speed + + + ); +}; diff --git a/application/ui/src/features/models/model-listing/components/model-row/dataset-revision-column.component.tsx b/application/ui/src/features/models/model-listing/components/model-row/dataset-revision-column.component.tsx new file mode 100644 index 0000000000..2fdaa23c33 --- /dev/null +++ b/application/ui/src/features/models/model-listing/components/model-row/dataset-revision-column.component.tsx @@ -0,0 +1,54 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { Flex, Text } from '@geti/ui'; +import { Image, Tag } from '@geti/ui/icons'; +import { useNumberFormatter } from 'react-aria'; + +import type { DatasetRevision } from '../../../../../constants/shared-types'; +import { formatDatasetRevisionDate } from '../../utils/date-formatting'; +import { ModelBadge } from './model-badge.component'; + +import styles from './model-row.module.scss'; + +type DatasetColumnProps = { + datasetRevision: DatasetRevision | undefined; + labelsCount: number | undefined; +}; + +export const DatasetColumn = ({ datasetRevision, labelsCount }: DatasetColumnProps) => { + const totalCount = datasetRevision?.item_counts?.total; + const formatter = useNumberFormatter(); + + // Should never happen, but just in case + if (datasetRevision === undefined) { + return ( + + Unknown + + ); + } + + return ( + + {datasetRevision.name} + + {formatDatasetRevisionDate(datasetRevision.created_at)} + + + {labelsCount !== undefined && ( + + + {labelsCount} + + )} + {totalCount !== undefined && ( + + + {formatter.format(totalCount)} + + )} + + + ); +}; diff --git a/application/ui/src/features/models/model-listing/components/model-row/model-badge.component.tsx b/application/ui/src/features/models/model-listing/components/model-row/model-badge.component.tsx new file mode 100644 index 0000000000..60de1d3aad --- /dev/null +++ b/application/ui/src/features/models/model-listing/components/model-row/model-badge.component.tsx @@ -0,0 +1,25 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { ReactNode } from 'react'; + +import { Badge, Flex, Text } from '@geti/ui'; + +import styles from './model-row.module.scss'; + +type ModelBadgeProps = { + children: ReactNode; + id?: string; +}; + +export const ModelBadge = ({ children, id }: ModelBadgeProps) => { + return ( + + + + {children} + + + + ); +}; diff --git a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.test.tsx b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.test.tsx index 4bc82b5fb6..36a92b8196 100644 --- a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.test.tsx +++ b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.test.tsx @@ -1,8 +1,9 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: Apache-2.0 -import { screen } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { getMockedDatasetRevision } from 'mocks/mock-dataset-revision'; import { render } from 'test-utils/render'; import { getMockedModel } from '../../../../../../mocks/mock-model'; @@ -25,19 +26,54 @@ describe('ModelRow', () => { }, }); + const datasetRevision = getMockedDatasetRevision({ + id: 'dataset-123', + name: 'Dataset 1', + item_counts: { + total: 10, + testing: 4, + training: 4, + validation: 2, + }, + }); + describe('basic rendering', () => { - it('should render all model information correctly', () => { - render(); + it('should render all model information correctly when grouped by architecture', () => { + render(); expect(screen.getByTestId('model-name')).toHaveTextContent('Test Model'); - expect(screen.getByText(/YOLOX \(Apache 2\.0\)/)).toBeInTheDocument(); + + const datasetBadge = screen.getByTestId('dataset-count'); + const labelsBadge = screen.getByTestId('labels-count'); + const labelSchemaRevision = defaultModel.training_info.label_schema_revision ?? {}; + const labelsCount = + 'labels' in labelSchemaRevision && Array.isArray(labelSchemaRevision.labels) + ? labelSchemaRevision.labels.length + : ''; + + expect(screen.getByText(datasetRevision.name)).toBeInTheDocument(); + expect(within(datasetBadge).getByText(datasetRevision.item_counts?.total?.toString() ?? '')); + expect(within(labelsBadge).getByText(labelsCount)); + expect(screen.queryByText(/YOLOX/)).not.toBeInTheDocument(); + expect(screen.queryByText('Speed')).not.toBeInTheDocument(); + }); + + it('should render all model information correctly when grouped by dataset', () => { + render(); + + expect(screen.getByTestId('model-name')).toHaveTextContent('Test Model'); + + expect(screen.getByText(/YOLOX/)).toBeInTheDocument(); expect(screen.getByText('Speed')).toBeInTheDocument(); + expect(screen.queryByText(datasetRevision.name)).not.toBeInTheDocument(); + expect(screen.queryByTestId('dataset-count')).not.toBeInTheDocument(); + expect(screen.queryByTestId('labels-count')).not.toBeInTheDocument(); }); it('should render "Unnamed Model" when model name is null or undefined', () => { const modelWithoutName = getMockedModel({ name: undefined }); - render(); + render(); expect(screen.getByTestId('model-name')).toHaveTextContent('Unnamed Model'); }); @@ -45,7 +81,7 @@ describe('ModelRow', () => { it('should render "-" when model size is 0 or negative', () => { const modelWithZeroSize = getMockedModel({ size: 0 }); - render(); + render(); expect(screen.getByText('-')).toBeInTheDocument(); }); @@ -57,7 +93,7 @@ describe('ModelRow', () => { }, }); - render(); + render(); expect(screen.getByText('Failed')).toBeInTheDocument(); }); @@ -65,13 +101,27 @@ describe('ModelRow', () => { describe('active model tag', () => { it('should show active tag only when model id matches activeModelArchitectureId', () => { - const { rerender } = render(); + const { rerender } = render( + + ); expect(screen.getByText('Active')).toBeInTheDocument(); - rerender(); + rerender( + + ); expect(screen.queryByText('Active')).not.toBeInTheDocument(); - rerender(); + rerender(); expect(screen.queryByText('Active')).not.toBeInTheDocument(); }); }); @@ -84,7 +134,15 @@ describe('ModelRow', () => { name: 'Parent Model', }); - render(); + render( + + ); expect(screen.getByText('Fine-tuned from')).toBeInTheDocument(); const parentLink = screen.getByRole('link', { name: 'Parent Model' }); @@ -95,7 +153,7 @@ describe('ModelRow', () => { }); it('should not render parent revision model when not provided', () => { - render(); + render(); expect(screen.queryByText('Fine-tuned from')).not.toBeInTheDocument(); }); diff --git a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx index 0fb4faccd5..5458ded7fb 100644 --- a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx +++ b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx @@ -1,17 +1,19 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: Apache-2.0 -import { Badge, Flex, Grid, Tag, Text } from '@geti/ui'; +import { Badge, Flex, Grid, Text } from '@geti/ui'; -import { ReactComponent as ThumbsUp } from '../../../../../assets/icons/thumbs-up.svg'; -import type { Model } from '../../../../../constants/shared-types'; +import type { DatasetRevision, Model } from '../../../../../constants/shared-types'; import { GRID_COLUMNS } from '../../constants'; import { AccuracyIndicator } from '../../model-variants/accuracy-indicator.component'; +import { type GroupByMode } from '../../types'; import { formatTrainingDateTime } from '../../utils/date-formatting'; import { formatModelSize } from '../../utils/format-model-size'; import { isFailedModel } from '../../utils/utils'; import { ActiveModelTag } from '../active-model-tag.component'; import { ParentRevisionModel } from '../parent-revision-model.component'; +import { ArchitectureColumn } from './architecture-column.component'; +import { DatasetColumn } from './dataset-revision-column.component'; import styles from './model-row.module.scss'; @@ -20,15 +22,29 @@ type ModelRowProps = { activeModelArchitectureId?: string; parentRevisionModel?: Model; onExpandModel?: (modelId: string) => void; + groupBy: GroupByMode; + datasetRevision: DatasetRevision | undefined; }; const FailedModel = () => { return Failed; }; -export const ModelRow = ({ model, activeModelArchitectureId, parentRevisionModel, onExpandModel }: ModelRowProps) => { +export const ModelRow = ({ + model, + activeModelArchitectureId, + parentRevisionModel, + onExpandModel, + groupBy, + datasetRevision, +}: ModelRowProps) => { const trainingEndTime = model.training_info.end_time; const totalSize = model.size; + const labelSchemaRevision = model.training_info.label_schema_revision ?? {}; + const labelsCount = + 'labels' in labelSchemaRevision && Array.isArray(labelSchemaRevision.labels) + ? labelSchemaRevision.labels.length + : undefined; return ( @@ -54,11 +70,11 @@ export const ModelRow = ({ model, activeModelArchitectureId, parentRevisionModel {formatTrainingDateTime(trainingEndTime)} - - {model.architecture} (Apache 2.0) - {/* TODO: Speed is hardcoded for now, once the backend is update we need to update this */} - } text={'Speed'} className={styles.recommendedForTag} /> - + {groupBy === 'architecture' ? ( + + ) : ( + + )} {totalSize > 0 ? formatModelSize(totalSize) : '-'} diff --git a/application/ui/src/features/models/model-listing/components/model-row/model-row.container.tsx b/application/ui/src/features/models/model-listing/components/model-row/model-row.container.tsx index 47fc566848..174652e723 100644 --- a/application/ui/src/features/models/model-listing/components/model-row/model-row.container.tsx +++ b/application/ui/src/features/models/model-listing/components/model-row/model-row.container.tsx @@ -12,8 +12,9 @@ type ModelRowContainerProps = { }; export const ModelRowContainer = ({ model }: ModelRowContainerProps) => { - const { activeModelArchitectureId, onExpandModel } = useModelListing(); + const { activeModelArchitectureId, onExpandModel, groupBy, datasetRevisions } = useModelListing(); const { data: parentRevisionModel } = useGetModel(model.parent_revision); + const datasetRevision = datasetRevisions.find(({ id }) => id === model.training_info.dataset_revision_id); return ( <> @@ -22,6 +23,8 @@ export const ModelRowContainer = ({ model }: ModelRowContainerProps) => { activeModelArchitectureId={activeModelArchitectureId} parentRevisionModel={parentRevisionModel} onExpandModel={onExpandModel} + groupBy={groupBy} + datasetRevision={datasetRevision} /> diff --git a/application/ui/src/features/models/model-listing/components/model-row/model-row.module.scss b/application/ui/src/features/models/model-listing/components/model-row/model-row.module.scss index 3f160be7f1..56073e2950 100644 --- a/application/ui/src/features/models/model-listing/components/model-row/model-row.module.scss +++ b/application/ui/src/features/models/model-listing/components/model-row/model-row.module.scss @@ -23,9 +23,21 @@ font-weight: 400; } -.recommendedForTag { - border-radius: var(--spectrum-global-dimension-size-50); - background-color: var(--spectrum-global-color-gray-300); - color: var(--spectrum-global-color-gray-700); +.datasetRevisionName { + font-size: var(--spectrum-global-dimension-font-size-100); font-weight: 400; } + +.datasetRevisionDate { + font-size: var(--spectrum-global-dimension-font-size-75); + color: var(--spectrum-global-color-gray-700); +} + +.modelBadge { + color: var(--spectrum-global-color-gray-800); + background-color: var(--spectrum-global-color-gray-200); + + & span[class*='Badge-label'] { + padding-inline-end: var(--spectrum-global-dimension-size-75); + } +} diff --git a/application/ui/src/features/models/model-listing/current-model-training/current-model-training.component.tsx b/application/ui/src/features/models/model-listing/current-model-training/current-model-training.component.tsx index 16a00699e0..160466c46b 100644 --- a/application/ui/src/features/models/model-listing/current-model-training/current-model-training.component.tsx +++ b/application/ui/src/features/models/model-listing/current-model-training/current-model-training.component.tsx @@ -4,10 +4,17 @@ import { dimensionValue, Flex, Heading, View } from '@geti/ui'; import { useCancelJob, useGetCurrentTrainingJob } from 'hooks/api/jobs.hook'; +import { type DatasetRevision } from '../../../../constants/shared-types'; import { ModelsTableHeader } from '../components/models-table-header.component'; +import { GroupByMode } from '../types'; import { TrainingModelRow } from './training-model-row.component'; -export const CurrentModelTraining = () => { +type CurrentModelTrainingProps = { + groupBy: GroupByMode; + datasetRevisions: DatasetRevision[]; +}; + +export const CurrentModelTraining = ({ groupBy, datasetRevisions }: CurrentModelTrainingProps) => { const activeTrainingJob = useGetCurrentTrainingJob(); const cancelJobMutation = useCancelJob(); @@ -35,7 +42,12 @@ export const CurrentModelTraining = () => { - + ); diff --git a/application/ui/src/features/models/model-listing/current-model-training/current-model-training.module.scss b/application/ui/src/features/models/model-listing/current-model-training/current-model-training.module.scss index b48234933d..f4ddff787c 100644 --- a/application/ui/src/features/models/model-listing/current-model-training/current-model-training.module.scss +++ b/application/ui/src/features/models/model-listing/current-model-training/current-model-training.module.scss @@ -37,9 +37,3 @@ .smallText { font-size: var(--spectrum-global-dimension-font-size-75); } - -.recommendedForTag { - border-radius: var(--spectrum-global-dimension-size-50); - background-color: var(--spectrum-global-color-gray-300); - color: var(--spectrum-global-color-gray-700); -} diff --git a/application/ui/src/features/models/model-listing/current-model-training/training-model-row.component.test.tsx b/application/ui/src/features/models/model-listing/current-model-training/training-model-row.component.test.tsx index 286250c769..b5f24c968e 100644 --- a/application/ui/src/features/models/model-listing/current-model-training/training-model-row.component.test.tsx +++ b/application/ui/src/features/models/model-listing/current-model-training/training-model-row.component.test.tsx @@ -1,7 +1,8 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: Apache-2.0 -import { screen } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; +import { getMockedDatasetRevision } from 'mocks/mock-dataset-revision'; import { getMockedModel } from 'mocks/mock-model'; import { HttpResponse } from 'msw'; import { render } from 'test-utils/render'; @@ -12,22 +13,30 @@ import { server } from '../../../../msw-node-setup'; import { TrainingModelRow } from './training-model-row.component'; describe('TrainingModelRow', () => { - const mockModels = [ - getMockedModel({ - id: 'model-123', - name: 'My Detection Model', - }), - ]; + const mockModels = getMockedModel({ + id: 'model-123', + name: 'My Detection Model', + training_info: { + dataset_revision_id: 'dataset-123', + status: 'in_progress', + label_schema_revision: { + labels: [ + { id: '1', name: 'car' }, + { id: '2', name: 'person' }, + ], + }, + }, + }); beforeEach(() => { server.use( - http.get('/api/projects/{project_id}/models', () => { + http.get('/api/projects/{project_id}/models/{model_id}', () => { return HttpResponse.json(mockModels); }) ); }); - it('renders all fields correctly', async () => { + it('renders all fields correctly when grouped by dataset', async () => { const job = getMockedJob({ metadata: { project: { id: '123' }, @@ -43,13 +52,77 @@ describe('TrainingModelRow', () => { started_at: '2026-01-19T08:15:00.000000+00:00', }); - render(); + const datasetRevision = getMockedDatasetRevision({ + id: 'dataset-123', + name: 'Dataset 1', + item_counts: { + total: 10, + testing: 4, + training: 4, + validation: 2, + }, + }); + + render(); expect(await screen.findByText('My Detection Model')).toBeVisible(); - expect(screen.getByText('Custom_Object_Detection_Gen3_ATSS')).toBeVisible(); expect(screen.getByText('Training')).toBeVisible(); expect(screen.getByText('Running...')).toBeVisible(); expect(screen.getByText(/Started: 19 Jan 2026/i)).toBeVisible(); + + expect(screen.getByText(/Custom_Object_Detection_Gen3_ATSS/)).toBeVisible(); + expect(screen.queryByText(datasetRevision.name)).not.toBeInTheDocument(); + expect(screen.queryByTestId('dataset-count')).not.toBeInTheDocument(); + expect(screen.queryByTestId('labels-count')).not.toBeInTheDocument(); + }); + + it('renders all fields correctly when grouped by architecture', async () => { + const job = getMockedJob({ + metadata: { + project: { id: '123' }, + model: { + id: 'model-123', + architecture: 'Custom_Object_Detection_Gen3_ATSS', + parent_revision_id: null, + dataset_revision_id: 'dataset-123', + }, + }, + status: 'RUNNING', + message: 'Running...', + started_at: '2026-01-19T08:15:00.000000+00:00', + }); + + const datasetRevision = getMockedDatasetRevision({ + id: 'dataset-123', + name: 'Dataset 1', + item_counts: { + total: 10, + testing: 4, + training: 4, + validation: 2, + }, + }); + + render(); + + expect(await screen.findByText('My Detection Model')).toBeVisible(); + expect(screen.getByText('Training')).toBeVisible(); + expect(screen.getByText('Running...')).toBeVisible(); + expect(screen.getByText(/Started: 19 Jan 2026/i)).toBeVisible(); + + expect(screen.queryByText(/Custom_Object_Detection_Gen3_ATSS/)).not.toBeInTheDocument(); + + expect(screen.getByText(datasetRevision.name)).toBeInTheDocument(); + const datasetBadge = screen.getByTestId('dataset-count'); + expect(within(datasetBadge).getByText(datasetRevision.item_counts?.total?.toString() ?? '')); + + const labelsBadge = screen.getByTestId('labels-count'); + const labelSchemaRevision = mockModels.training_info.label_schema_revision ?? {}; + const labelsCount = + 'labels' in labelSchemaRevision && Array.isArray(labelSchemaRevision.labels) + ? labelSchemaRevision.labels.length + : ''; + expect(within(labelsBadge).getByText(labelsCount)); }); it('renders Cancel button when onCancel is provided and job is running', async () => { @@ -67,7 +140,7 @@ describe('TrainingModelRow', () => { status: 'RUNNING', }); - render(); + render(); const cancelButton = await screen.findByRole('button', { name: /cancel training job/i }); expect(cancelButton).toBeVisible(); @@ -89,7 +162,7 @@ describe('TrainingModelRow', () => { status: 'FINISHED', }); - render(); + render(); const cancelButton = await screen.findByRole('button', { name: /cancel training job/i }); expect(cancelButton).toBeDisabled(); @@ -114,7 +187,7 @@ describe('TrainingModelRow', () => { }, }); - render(); + render(); expect(await screen.findByText('unknown-model-id')).toBeVisible(); }); diff --git a/application/ui/src/features/models/model-listing/current-model-training/training-model-row.component.tsx b/application/ui/src/features/models/model-listing/current-model-training/training-model-row.component.tsx index 8e70789eb3..2d3a44a672 100644 --- a/application/ui/src/features/models/model-listing/current-model-training/training-model-row.component.tsx +++ b/application/ui/src/features/models/model-listing/current-model-training/training-model-row.component.tsx @@ -1,14 +1,18 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: Apache-2.0 -import { Button, Divider, Flex, Grid, Loading, Tag, Text } from '@geti/ui'; +import { useState } from 'react'; + +import { AlertDialog, Button, DialogContainer, Divider, Flex, Grid, Loading, Tag, Text } from '@geti/ui'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; -import { ReactComponent as ThumbsUp } from '../../../../assets/icons/thumbs-up.svg'; -import type { Job } from '../../../../constants/shared-types'; -import { useGetModels } from '../../hooks/api/use-get-models.hook'; +import type { DatasetRevision, Job } from '../../../../constants/shared-types'; +import { useGetModel } from '../../hooks/api/use-get-model.hook'; +import { ArchitectureColumn } from '../components/model-row/architecture-column.component'; +import { DatasetColumn } from '../components/model-row/dataset-revision-column.component'; import { GRID_COLUMNS } from '../constants'; +import { GroupByMode } from '../types'; import { BottomProgressBar } from './bottom-progress-bar.component'; import classes from './current-model-training.module.scss'; @@ -18,6 +22,8 @@ dayjs.extend(duration); type TrainingModelRowProps = { job: Job; onCancel?: () => void; + groupBy: GroupByMode; + datasetRevisions: DatasetRevision[]; }; const TrainingTag = () => ( @@ -28,13 +34,54 @@ const StatusTag = ({ status }: { status: string }) => ( ); -export const TrainingModelRow = ({ job, onCancel }: TrainingModelRowProps) => { - const { data: models } = useGetModels(); - const modelId = 'model' in job.metadata && job.metadata.model?.id; +type CancelTrainingProps = { + job: Job; + onCancel: () => void; +}; + +const CancelTraining = ({ job, onCancel }: CancelTrainingProps) => { + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + return ( + <> + + setIsDeleteDialogOpen(false)}> + {isDeleteDialogOpen && ( + + Are you sure you want to cancel training job? + + )} + + + ); +}; + +export const TrainingModelRow = ({ job, onCancel, datasetRevisions, groupBy }: TrainingModelRowProps) => { + const modelId = 'model' in job.metadata ? job.metadata.model?.id : undefined; + const { data: trainingModel } = useGetModel(modelId); const modelArchitecture = 'model' in job.metadata && job.metadata.model?.architecture; - const trainingModel = models?.find((model) => model.id === modelId); const modelName = trainingModel?.name || modelId; + const datasetRevision = datasetRevisions.find(({ id }) => id === trainingModel?.training_info.dataset_revision_id); + const labelSchemaRevision = trainingModel?.training_info.label_schema_revision ?? {}; + const labelsCount = + 'labels' in labelSchemaRevision && Array.isArray(labelSchemaRevision.labels) + ? labelSchemaRevision.labels.length + : undefined; + return ( { ... - {modelArchitecture} - {/* TODO: Speed is hardcoded for now, once the backend is update we need to update this */} - } text={'Speed'} className={classes.recommendedForTag} /> + {groupBy === 'architecture' ? ( + + ) : ( + + )} ... ... - {onCancel ? ( - - ) : ( -
- )} + {onCancel ? :
} ); diff --git a/application/ui/src/features/models/model-listing/model-listing.container.tsx b/application/ui/src/features/models/model-listing/model-listing.container.tsx index 18c1c2d537..7e0d7c2b91 100644 --- a/application/ui/src/features/models/model-listing/model-listing.container.tsx +++ b/application/ui/src/features/models/model-listing/model-listing.container.tsx @@ -13,7 +13,7 @@ import { ModelListing } from './model-listing.component'; import { ModelListingProvider, useModelListing } from './provider/model-listing-provider'; const ModelListingContent = () => { - const { groupedModels, searchBy } = useModelListing(); + const { groupedModels, searchBy, datasetRevisions, groupBy } = useModelListing(); const trainingJob = useGetCurrentTrainingJob(); const hasNoResults = groupedModels.length === 0 && searchBy.length > 0; @@ -28,7 +28,7 @@ const ModelListingContent = () => { justifyContent={'center'} UNSAFE_style={{ padding: dimensionValue('size-300') }} > - + {isEmpty(trainingJob) && ( @@ -47,7 +47,7 @@ const ModelListingContent = () => { - + diff --git a/application/ui/src/features/models/model-listing/provider/model-listing-provider.tsx b/application/ui/src/features/models/model-listing/provider/model-listing-provider.tsx index 598a471b92..2a38ac289b 100644 --- a/application/ui/src/features/models/model-listing/provider/model-listing-provider.tsx +++ b/application/ui/src/features/models/model-listing/provider/model-listing-provider.tsx @@ -5,6 +5,7 @@ import { createContext, ReactNode, useContext, useState } from 'react'; import { useGetDatasetRevisions } from 'hooks/use-get-dataset-revisions.hook'; +import { type DatasetRevision } from '../../../../constants/shared-types'; import { useGetActiveModelArchitectureId } from '../../hooks/api/use-get-active-model-architecture-id.hook'; import { useGetModels } from '../../hooks/api/use-get-models.hook'; import { useGroupedModels } from '../hooks/use-grouped-models.hook'; @@ -19,6 +20,7 @@ interface ModelListingContextValue { activeModelArchitectureId: string | undefined; groupedModels: GroupedModels[]; searchBy: string; + datasetRevisions: DatasetRevision[]; // Actions onGroupByChange: (mode: GroupByMode) => void; @@ -84,6 +86,7 @@ export const ModelListingProvider = ({ children }: ModelListingProviderProps) => activeModelArchitectureId, groupedModels, searchBy, + datasetRevisions, onGroupByChange, onSortChange, onPinActiveToggle, diff --git a/application/ui/src/features/models/model-listing/utils/date-formatting.ts b/application/ui/src/features/models/model-listing/utils/date-formatting.ts index 9fca6854e5..0217e3b1ae 100644 --- a/application/ui/src/features/models/model-listing/utils/date-formatting.ts +++ b/application/ui/src/features/models/model-listing/utils/date-formatting.ts @@ -18,3 +18,7 @@ export const formatTrainingDateTime = (dateString: string | null | undefined): s return '-'; } }; + +export const formatDatasetRevisionDate = (date: string): string => { + return dayjs(date).format('DD MMM YYYY, hh:mm A'); +}; From d98cb0fde37dc5441259fc5eabfbab21ba8d61f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Weso=C5=82owski?= Date: Thu, 5 Feb 2026 16:03:42 +0100 Subject: [PATCH 4/6] chore: use camel case --- .../components/model-actions/model-actions.component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/ui/src/features/models/model-listing/components/model-actions/model-actions.component.tsx b/application/ui/src/features/models/model-listing/components/model-actions/model-actions.component.tsx index b0ac25e554..129ad1deb2 100644 --- a/application/ui/src/features/models/model-listing/components/model-actions/model-actions.component.tsx +++ b/application/ui/src/features/models/model-listing/components/model-actions/model-actions.component.tsx @@ -33,7 +33,7 @@ export const ModelActions = ({ model }: ModelActionsProps) => { const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const disabled_keys = isFailedModel(model) ? [MODEL_ACTIONS.ACTIVE, MODEL_ACTIONS.RENAME] : []; + const disabledKeys = isFailedModel(model) ? [MODEL_ACTIONS.ACTIVE, MODEL_ACTIONS.RENAME] : []; const handleAction = (key: Key) => { if (key === MODEL_ACTIONS.ACTIVE) { @@ -72,7 +72,7 @@ export const ModelActions = ({ model }: ModelActionsProps) => { - + Set as active Rename Delete From 5496cc80cf6a6283a140ae07070a16a457a40794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Weso=C5=82owski?= Date: Thu, 5 Feb 2026 16:20:42 +0100 Subject: [PATCH 5/6] add space only when failed model is there --- .../components/model-row/model-row.component.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx index 5458ded7fb..79d4fd9f06 100644 --- a/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx +++ b/application/ui/src/features/models/model-listing/components/model-row/model-row.component.tsx @@ -51,7 +51,13 @@ export const ModelRow = ({ - {model.name ?? 'Unnamed Model'} {isFailedModel(model) && } + {model.name ?? 'Unnamed Model'} + {isFailedModel(model) && ( + <> + {' '} + + + )} {model.id === activeModelArchitectureId && } From 33f1b0a2b2de9cfe88e8ccf3a185f55cc54adf0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Weso=C5=82owski?= Date: Fri, 6 Feb 2026 08:39:54 +0100 Subject: [PATCH 6/6] rename styles to classes, remove default align items --- .../components/model-row/architecture-column.component.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/ui/src/features/models/model-listing/components/model-row/architecture-column.component.tsx b/application/ui/src/features/models/model-listing/components/model-row/architecture-column.component.tsx index 26bde125e6..ec5d28b266 100644 --- a/application/ui/src/features/models/model-listing/components/model-row/architecture-column.component.tsx +++ b/application/ui/src/features/models/model-listing/components/model-row/architecture-column.component.tsx @@ -6,7 +6,7 @@ import { Flex, Text } from '@geti/ui'; import { ReactComponent as ThumbsUp } from '../../../../../assets/icons/thumbs-up.svg'; import { ModelBadge } from './model-badge.component'; -import styles from './model-row.module.scss'; +import classes from './model-row.module.scss'; type ArchitectureColumnProps = { architecture: string; @@ -14,8 +14,8 @@ type ArchitectureColumnProps = { export const ArchitectureColumn = ({ architecture }: ArchitectureColumnProps) => { return ( - - {architecture} (Apache 2.0) + + {architecture} (Apache 2.0) {/* TODO: Speed is hardcoded for now, once the backend is update we need to update this */}