diff --git a/clients/ui/bff/internal/mocks/static_data_mock.go b/clients/ui/bff/internal/mocks/static_data_mock.go index e633059e72..63f10191f0 100644 --- a/clients/ui/bff/internal/mocks/static_data_mock.go +++ b/clients/ui/bff/internal/mocks/static_data_mock.go @@ -1333,14 +1333,17 @@ func CreateSampleCatalogSource(id string, name string, catalogType string) model defaultCatalog := id == "catalog-1" sourceConfig := models.CatalogSourceConfig{ - Name: name, - Id: id, - Type: catalogType, - Enabled: BoolPtr(true), - Labels: []string{"source-1"}, - IsDefault: &defaultCatalog, - IncludedModels: []string{"rhelai1/modelcar-granite-7b-starter"}, - ExcludedModels: []string{"model-a:1.0", "model-b:*"}, + Name: name, + Id: id, + Type: catalogType, + Enabled: BoolPtr(true), + Labels: []string{"source-1"}, + IsDefault: &defaultCatalog, + } + + if !defaultCatalog { + sourceConfig.IncludedModels = []string{"rhelai1/modelcar-granite-7b-starter"} + sourceConfig.ExcludedModels = []string{"model-a:1.0", "model-b:*"} } switch catalogType { diff --git a/clients/ui/frontend/src/__mocks__/index.ts b/clients/ui/frontend/src/__mocks__/index.ts index 8114e1d3fa..b336da531d 100644 --- a/clients/ui/frontend/src/__mocks__/index.ts +++ b/clients/ui/frontend/src/__mocks__/index.ts @@ -3,5 +3,6 @@ export * from './mockModelVersion'; export * from './mockModelVersionList'; export * from './mockModelArtifactList'; export * from './mockCatalogSourceList'; +export * from './mockCatalogSourceConfigList'; export * from './mockCatalogModelList'; export * from './mockCatalogModelArtifactList'; diff --git a/clients/ui/frontend/src/__mocks__/mockCatalogSourceConfigList.ts b/clients/ui/frontend/src/__mocks__/mockCatalogSourceConfigList.ts new file mode 100644 index 0000000000..593240a60b --- /dev/null +++ b/clients/ui/frontend/src/__mocks__/mockCatalogSourceConfigList.ts @@ -0,0 +1,74 @@ +import { + CatalogSourceConfig, + CatalogSourceConfigList, + YamlCatalogSourceConfig, + HuggingFaceCatalogSourceConfig, + CatalogSourceType, +} from '~/app/modelCatalogTypes'; + +export const mockYamlCatalogSourceConfig = ( + partial?: Partial, +): YamlCatalogSourceConfig => ({ + id: 'yaml-source-1', + name: 'Red Hat AI', + type: CatalogSourceType.YAML, + enabled: true, + labels: ['Red Hat AI'], + includedModels: [], + excludedModels: [], + isDefault: true, + yaml: 'version: 1.0\nmodels:\n - name: example-model', + ...partial, +}); + +export const mockHuggingFaceCatalogSourceConfig = ( + partial?: Partial, +): HuggingFaceCatalogSourceConfig => ({ + id: 'huggingface-source-1', + name: 'Huggingface_Admin_1', + type: CatalogSourceType.HUGGING_FACE, + enabled: true, + labels: ['Hugging Face'], + includedModels: [], + excludedModels: [], + isDefault: false, + allowedOrganization: 'Google', + apiKey: undefined, + ...partial, +}); + +export const mockCatalogSourceConfig = ( + partial?: Partial, +): CatalogSourceConfig => { + if (partial?.type === CatalogSourceType.HUGGING_FACE) { + return mockHuggingFaceCatalogSourceConfig(partial as Partial); + } + return mockYamlCatalogSourceConfig(partial as Partial); +}; + +export const mockCatalogSourceConfigList = ( + partial?: Partial, +): CatalogSourceConfigList => ({ + catalogs: [ + mockYamlCatalogSourceConfig({ id: 'red-hat-ai', name: 'Red Hat AI', isDefault: true }), + mockYamlCatalogSourceConfig({ + id: 'red-hat-ai-validated', + name: 'Red Hat AI validated', + isDefault: true, + }), + mockHuggingFaceCatalogSourceConfig({ + id: 'huggingface-admin-1', + name: 'Huggingface_Admin_1', + allowedOrganization: 'Google', + isDefault: false, + }), + mockYamlCatalogSourceConfig({ + id: 'yaml-amdimport-1', + name: 'YAMLAmdImport_1', + isDefault: false, + includedModels: ['model1', 'model2'], + excludedModels: ['model3'], + }), + ], + ...partial, +}); diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelCatalogSettings.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelCatalogSettings.ts index 2ee4b5a391..8c82d1afee 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelCatalogSettings.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/modelCatalogSettings.ts @@ -1,4 +1,75 @@ import { appChrome } from './appChrome'; +import { TableRow } from './components/table'; + +class CatalogSourceConfigRow extends TableRow { + findName() { + return this.find().find('[data-label="Name"]'); + } + + findOrganization() { + return this.find().find('[data-label="Organization"]'); + } + + findModelVisibility() { + return this.find().find('[data-label="Model visibility"]'); + } + + findSourceType() { + return this.find().find('[data-label="Source type"]'); + } + + findEnableToggle() { + return this.find().find('[data-label="Enable"]').find('input[type="checkbox"]'); + } + + findValidationStatus() { + return this.find().find('[data-label="Validation status"]'); + } + + findManageSourceButton() { + return this.find() + .find('[data-label="Actions"]') + .findByRole('button', { name: 'Manage source' }); + } + + shouldHaveModelVisibility(visibility: 'Filtered' | 'Unfiltered') { + this.findModelVisibility().contains(visibility); + return this; + } + + shouldHaveOrganization(org: string) { + this.findOrganization().contains(org); + return this; + } + + shouldHaveSourceType(type: string) { + this.findSourceType().contains(type); + return this; + } + + toggleEnable() { + this.findEnableToggle().click({ force: true }); + return this; + } + + shouldHaveEnableToggle(shouldExist: boolean) { + if (shouldExist) { + this.findEnableToggle().should('exist'); + } else { + this.find().find('[data-label="Enable"]').should('be.empty'); + } + return this; + } + + shouldHaveEnableState(enabled: boolean) { + if (enabled) { + this.findEnableToggle().should('be.checked'); + } else { + this.findEnableToggle().should('not.be.checked'); + } + return this; + } +} class ModelCatalogSettings { visit(wait = true) { @@ -38,6 +109,35 @@ class ModelCatalogSettings { findAddSourceButton() { return cy.findByTestId('add-source-button'); } + + findTable() { + return cy.findByTestId('catalog-source-configs-table'); + } + + findEmptyState() { + return cy.findByTestId('catalog-settings-empty-state'); + } + + getRow(name: string) { + return new CatalogSourceConfigRow(() => + this.findTable().find('tbody').find('tr').contains(name).parents('tr'), + ); + } + + findRows() { + return this.findTable().find('tbody tr'); + } + + shouldHaveSourceConfigs() { + this.findTable().should('exist'); + this.findRows().should('have.length.at.least', 1); + return this; + } + + shouldBeEmpty() { + this.findEmptyState().should('exist'); + return this; + } } class ManageSourcePage { diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalogSettings/modelCatalogSettings.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalogSettings/modelCatalogSettings.cy.ts index f1532f85f1..3135f5202f 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalogSettings/modelCatalogSettings.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelCatalogSettings/modelCatalogSettings.cy.ts @@ -3,8 +3,14 @@ import { manageSourcePage, } from '~/__tests__/cypress/cypress/pages/modelCatalogSettings'; import { MODEL_CATALOG_API_VERSION } from '~/__tests__/cypress/cypress/support/commands/api'; -import { mockCatalogSource, mockCatalogSourceList } from '~/__mocks__'; -import type { CatalogSource } from '~/app/modelCatalogTypes'; +import { + mockCatalogSource, + mockCatalogSourceList, + mockCatalogSourceConfigList, + mockYamlCatalogSourceConfig, + mockHuggingFaceCatalogSourceConfig, +} from '~/__mocks__'; +import type { CatalogSource, CatalogSourceConfig } from '~/app/modelCatalogTypes'; const NAMESPACE = 'kubeflow'; const userMock = { @@ -14,7 +20,7 @@ const userMock = { }, }; -const setupMocks = (sources: CatalogSource[] = []) => { +const setupMocks = (sources: CatalogSource[] = [], sourceConfigs: CatalogSourceConfig[] = []) => { cy.intercept('GET', '/model-registry/api/v1/namespaces', { data: [{ metadata: { name: NAMESPACE } }], }); @@ -28,6 +34,18 @@ const setupMocks = (sources: CatalogSource[] = []) => { items: sources, }), ); + cy.intercept( + 'GET', + `/model-registry/api/${MODEL_CATALOG_API_VERSION}/settings/model_catalog/source_configs*`, + { + statusCode: 200, + body: { + data: mockCatalogSourceConfigList({ + catalogs: sourceConfigs, + }), + }, + }, + ).as('getCatalogSourceConfigs'); }; function selectNamespaceIfPresent() { @@ -40,8 +58,14 @@ function selectNamespaceIfPresent() { } describe('Model Catalog Settings', () => { + const defaultYamlSource = mockYamlCatalogSourceConfig({ + id: 'default-yaml', + name: 'Default Catalog', + isDefault: true, + }); + beforeEach(() => { - setupMocks(); + setupMocks([], [defaultYamlSource]); }); it('should display the settings page', () => { @@ -78,6 +102,193 @@ describe('Model Catalog Settings', () => { }); }); +describe('Catalog Source Configs Table', () => { + const defaultYamlSource = mockYamlCatalogSourceConfig({ + id: 'default-yaml', + name: 'Default Catalog', + isDefault: true, + enabled: true, + includedModels: [], + excludedModels: [], + }); + + const huggingFaceSource = mockHuggingFaceCatalogSourceConfig({ + id: 'hf-google', + name: 'HuggingFace Google', + isDefault: false, + enabled: true, + allowedOrganization: 'Google', + includedModels: ['model1', 'model2'], + }); + + const customYamlSource = mockYamlCatalogSourceConfig({ + id: 'custom-yaml', + name: 'Custom YAML', + isDefault: false, + enabled: false, + excludedModels: ['excluded-model'], + }); + + beforeEach(() => { + setupMocks([], [defaultYamlSource, huggingFaceSource, customYamlSource]); + }); + + it('should display empty state when no source configs exist', () => { + setupMocks([], []); + modelCatalogSettings.visit(); + modelCatalogSettings.shouldBeEmpty(); + modelCatalogSettings.findEmptyState().should('contain', 'No catalog sources'); + }); + + it('should display table with source configs', () => { + modelCatalogSettings.visit(); + modelCatalogSettings.shouldHaveSourceConfigs(); + modelCatalogSettings.findRows().should('have.length', 3); + }); + + it('should render table column headers correctly', () => { + modelCatalogSettings.visit(); + modelCatalogSettings.findTable().should('be.visible'); + modelCatalogSettings.findTable().contains('th', 'Name').should('be.visible'); + modelCatalogSettings.findTable().contains('th', 'Organization').should('be.visible'); + modelCatalogSettings.findTable().contains('th', 'Model visibility').should('be.visible'); + modelCatalogSettings.findTable().contains('th', 'Source type').should('be.visible'); + modelCatalogSettings.findTable().contains('th', 'Enable').should('be.visible'); + modelCatalogSettings.findTable().contains('th', 'Validation status').should('be.visible'); + }); + + describe('Table row rendering', () => { + it('should render default YAML source correctly', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('Default Catalog'); + row.findName().should('be.visible').and('contain', 'Default Catalog'); + row.shouldHaveOrganization('-'); + row.shouldHaveModelVisibility('Unfiltered'); + row.shouldHaveSourceType('YAML file'); + row.shouldHaveEnableToggle(false); // Default sources don't have toggle + }); + + it('should render Hugging Face source correctly', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('HuggingFace Google'); + row.findName().should('be.visible').and('contain', 'HuggingFace Google'); + row.shouldHaveOrganization('Google'); + row.shouldHaveModelVisibility('Filtered'); + row.shouldHaveSourceType('Hugging Face'); + row.shouldHaveEnableToggle(true); + row.shouldHaveEnableState(true); + }); + + it('should render custom YAML source correctly', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('Custom YAML'); + row.findName().should('be.visible').and('contain', 'Custom YAML'); + row.shouldHaveOrganization('-'); + row.shouldHaveModelVisibility('Filtered'); + row.shouldHaveSourceType('YAML file'); + row.shouldHaveEnableToggle(true); + row.shouldHaveEnableState(false); + }); + }); + + describe('Enable toggle functionality', () => { + it('should show alert when enable toggle is clicked', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('HuggingFace Google'); + row.findName().should('be.visible'); + row.findEnableToggle().should('exist').and('be.checked'); + + cy.window().then((win) => { + cy.stub(win, 'alert').as('windowAlert'); + }); + + row.toggleEnable(); + + cy.get('@windowAlert').should( + 'have.been.calledWith', + 'Toggle clicked! "HuggingFace Google" will be disabled when functionality is implemented.', + ); + }); + + it('should not show toggle for default sources', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('Default Catalog'); + row.findName().should('be.visible'); + row.shouldHaveEnableToggle(false); + }); + }); + + describe('Manage source button', () => { + it('should navigate to manage source page when button is clicked', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('HuggingFace Google'); + row.findName().should('be.visible'); + row.findManageSourceButton().should('be.visible').click(); + cy.url().should('include', '/model-catalog-settings/manage-source/hf-google'); + manageSourcePage.findManageSourceTitle(); + }); + + it('should navigate to correct manage source page for each row', () => { + modelCatalogSettings.visit(); + const customRow = modelCatalogSettings.getRow('Custom YAML'); + customRow.findName().should('be.visible'); + customRow.findManageSourceButton().should('be.visible').click(); + cy.url().should('include', '/model-catalog-settings/manage-source/custom-yaml'); + }); + }); + + describe('Kebab menu actions', () => { + it('should show delete action for non-default sources', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('HuggingFace Google'); + row.findName().should('be.visible'); + row.findKebab().should('be.visible').click(); + cy.findByRole('menuitem', { name: 'Delete source' }) + .should('be.visible') + .and('not.be.disabled'); + }); + + it('should disable delete action for default sources', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('Default Catalog'); + row.findName().should('be.visible'); + row.findKebab().should('be.visible').click(); + cy.findByRole('menuitem', { name: 'Delete source' }).should('be.visible').and('be.disabled'); + }); + }); + + describe('Model visibility badges', () => { + it('should show "Filtered" badge when source has included models', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('HuggingFace Google'); + row.findName().should('be.visible'); + row.findModelVisibility().should('be.visible').and('contain', 'Filtered'); + row + .findModelVisibility() + .find('[data-testid*="model-visibility-filtered"]') + .should('be.visible'); + }); + + it('should show "Filtered" badge when source has excluded models', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('Custom YAML'); + row.findName().should('be.visible'); + row.findModelVisibility().should('be.visible').and('contain', 'Filtered'); + }); + + it('should show "Unfiltered" badge when source has no filters', () => { + modelCatalogSettings.visit(); + const row = modelCatalogSettings.getRow('Default Catalog'); + row.findName().should('be.visible'); + row.findModelVisibility().should('be.visible').and('contain', 'Unfiltered'); + row + .findModelVisibility() + .find('[data-testid*="model-visibility-unfiltered"]') + .should('be.visible'); + }); + }); +}); + describe('Manage Source Page', () => { beforeEach(() => { setupMocks(); diff --git a/clients/ui/frontend/src/app/modelCatalogTypes.ts b/clients/ui/frontend/src/app/modelCatalogTypes.ts index c8d083a367..e8e1e38d77 100644 --- a/clients/ui/frontend/src/app/modelCatalogTypes.ts +++ b/clients/ui/frontend/src/app/modelCatalogTypes.ts @@ -70,6 +70,11 @@ export enum SourceLabel { other = 'null', } +export enum CatalogSourceType { + YAML = 'yaml', + HUGGING_FACE = 'huggingface', +} + export type CatalogArtifactBase = { createTimeSinceEpoch: string; lastUpdateTimeSinceEpoch: string; @@ -243,12 +248,12 @@ export type CatalogSourceConfigCommon = { }; export type YamlCatalogSourceConfig = CatalogSourceConfigCommon & { - type: 'yaml'; + type: CatalogSourceType.YAML; yaml?: string; }; export type HuggingFaceCatalogSourceConfig = CatalogSourceConfigCommon & { - type: 'huggingface'; + type: CatalogSourceType.HUGGING_FACE; allowedOrganization?: string; apiKey?: string; }; diff --git a/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTable.tsx b/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTable.tsx new file mode 100644 index 0000000000..569166219e --- /dev/null +++ b/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTable.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { Button, Toolbar, ToolbarContent, ToolbarItem } from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { Table } from 'mod-arch-shared'; +import { CatalogSourceConfig } from '~/app/modelCatalogTypes'; +import { catalogSourceConfigsColumns } from './CatalogSourceConfigsTableColumns'; +import CatalogSourceConfigsTableRow from './CatalogSourceConfigsTableRow'; + +type CatalogSourceConfigsTableProps = { + catalogSourceConfigs: CatalogSourceConfig[]; + onAddSource: () => void; + onDeleteSource?: (config: CatalogSourceConfig) => void; +}; + +const CatalogSourceConfigsTable: React.FC = ({ + catalogSourceConfigs, + onAddSource, + onDeleteSource, +}) => ( + + + + + + + + } + rowRenderer={(config) => ( + + )} + variant="compact" + /> +); + +export default CatalogSourceConfigsTable; diff --git a/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTableColumns.tsx b/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTableColumns.tsx new file mode 100644 index 0000000000..4a5bd42364 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTableColumns.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { List, ListItem } from '@patternfly/react-core'; +import { kebabTableColumn, SortableData } from 'mod-arch-shared'; +import { CatalogSourceConfig } from '~/app/modelCatalogTypes'; + +export const catalogSourceConfigsColumns: SortableData[] = [ + { + field: 'name', + label: 'Name', + sortable: (a, b) => a.name.localeCompare(b.name), + width: 15, + }, + { + field: 'allowedOrganization', + label: 'Organization', + sortable: false, + info: { + popover: + 'Applies only to Hugging Face sources. Shows the organization the source syncs models from (for example, Google). Only models within this organization are included in the catalog.', + }, + width: 15, + }, + { + field: 'filters', + label: 'Model visibility', + sortable: false, + info: { + popover: ( +
+

+ Shows whether all models from a source appear in the model catalog or if visibility is + filtered. +

+ + + All models: Every model from the source appears in the catalog. + + + Filtered: Only specific models appear, based on the visibility + settings for that source. + + +
+ ), + }, + width: 15, + }, + { + field: 'type', + label: 'Source type', + sortable: (a, b) => a.type.localeCompare(b.type), + width: 15, + }, + { + field: 'enabled', + label: 'Enable', + sortable: false, + info: { + popover: + 'Enable a source to make its models available to users in your organization from the model catalog.', + }, + width: 10, + }, + { + field: 'status', + label: 'Validation status', + sortable: false, + width: 10, + }, + { + field: 'actions', + label: '', + sortable: false, + width: 15, + }, + kebabTableColumn(), +]; diff --git a/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTableRow.tsx b/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTableRow.tsx new file mode 100644 index 0000000000..1c21c38ba4 --- /dev/null +++ b/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/CatalogSourceConfigsTableRow.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import { ActionsColumn, Td, Tr } from '@patternfly/react-table'; +import { Button, Label, Switch } from '@patternfly/react-core'; +import { useNavigate } from 'react-router-dom'; +import { CatalogSourceConfig } from '~/app/modelCatalogTypes'; +import { manageSourceUrl } from '~/app/routes/modelCatalogSettings/modelCatalogSettings'; +import { + CATALOG_SOURCE_TYPE_LABELS, + ModelVisibilityBadgeColor, +} from '~/concepts/modelCatalogSettings/const'; +import { hasSourceFilters, getOrganizationDisplay } from '~/concepts/modelCatalogSettings/utils'; + +type CatalogSourceConfigsTableRowProps = { + catalogSourceConfig: CatalogSourceConfig; + onDelete?: (config: CatalogSourceConfig) => void; +}; + +const CatalogSourceConfigsTableRow: React.FC = ({ + catalogSourceConfig, + onDelete, +}) => { + const navigate = useNavigate(); + const isDefault = catalogSourceConfig.isDefault ?? false; + const isEnabled = catalogSourceConfig.enabled ?? true; + + const hasFilters = React.useMemo( + () => hasSourceFilters(catalogSourceConfig), + [catalogSourceConfig], + ); + + const handleEnableToggle = (checked: boolean) => { + // TODO: Implement actual enable/disable functionality + window.alert( + `Toggle clicked! "${catalogSourceConfig.name}" will be ${checked ? 'enabled' : 'disabled'} when functionality is implemented.`, + ); + }; + + const handleManageSource = () => { + navigate(manageSourceUrl(catalogSourceConfig.id)); + }; + + const handleDeleteSource = () => { + // TODO: - Implement actual delete functionality + onDelete?.(catalogSourceConfig); + }; + + const organizationValue = getOrganizationDisplay(catalogSourceConfig, isDefault); + + return ( +
+ + + + + + + + + + ); +}; + +export default CatalogSourceConfigsTableRow; diff --git a/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/ModelCatalogSettings.tsx b/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/ModelCatalogSettings.tsx index 7c7395eeda..c994328f95 100644 --- a/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/ModelCatalogSettings.tsx +++ b/clients/ui/frontend/src/app/pages/modelCatalogSettings/screens/ModelCatalogSettings.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Button, Flex, FlexItem } from '@patternfly/react-core'; +import { Button, EmptyState, EmptyStateBody, EmptyStateVariant } from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons'; import { useNavigate } from 'react-router-dom'; import { ProjectObjectType, TitleWithIcon, ApplicationsPage } from 'mod-arch-shared'; @@ -9,23 +9,15 @@ import { addSourceUrl, } from '~/app/routes/modelCatalogSettings/modelCatalogSettings'; import { ModelCatalogSettingsContext } from '~/app/context/modelCatalogSettings/ModelCatalogSettingsContext'; +import CatalogSourceConfigsTable from './CatalogSourceConfigsTable'; const ModelCatalogSettings: React.FC = () => { const navigate = useNavigate(); const { catalogSourceConfigs, catalogSourceConfigsLoaded, catalogSourceConfigsLoadError } = React.useContext(ModelCatalogSettingsContext); - // Log the source configs for verification - React.useEffect(() => { - if (catalogSourceConfigsLoaded && catalogSourceConfigs) { - // eslint-disable-next-line no-console - console.log('Catalog Source Configs:', catalogSourceConfigs); - } - if (catalogSourceConfigsLoadError) { - // eslint-disable-next-line no-console - console.error('Error loading catalog source configs:', catalogSourceConfigsLoadError); - } - }, [catalogSourceConfigs, catalogSourceConfigsLoaded, catalogSourceConfigsLoadError]); + const configs = catalogSourceConfigs?.catalogs || []; + const isEmpty = catalogSourceConfigsLoaded && configs.length === 0; return ( { /> } description={CATALOG_SETTINGS_DESCRIPTION} - empty={false} - loaded - provideChildrenPadding - > - - + empty={isEmpty} + emptyStatePage={ + + + No catalog sources have been configured. Add a source to get started. + - - + + } + loaded={catalogSourceConfigsLoaded} + loadError={catalogSourceConfigsLoadError} + errorMessage="Unable to load catalog source configurations." + provideChildrenPadding + > + navigate(addSourceUrl())} + /> ); }; diff --git a/clients/ui/frontend/src/concepts/modelCatalogSettings/const.ts b/clients/ui/frontend/src/concepts/modelCatalogSettings/const.ts new file mode 100644 index 0000000000..925dbe0bbf --- /dev/null +++ b/clients/ui/frontend/src/concepts/modelCatalogSettings/const.ts @@ -0,0 +1,20 @@ +import { + CatalogSourceConfig, + HuggingFaceCatalogSourceConfig, + CatalogSourceType, +} from '~/app/modelCatalogTypes'; + +export const CATALOG_SOURCE_TYPE_LABELS: Record = { + [CatalogSourceType.YAML]: 'YAML file', + [CatalogSourceType.HUGGING_FACE]: 'Hugging Face', +}; + +export enum ModelVisibilityBadgeColor { + FILTERED = 'blue', + UNFILTERED = 'grey', +} + +// Type guard for Hugging Face sources +export const isHuggingFaceSource = ( + config: CatalogSourceConfig, +): config is HuggingFaceCatalogSourceConfig => config.type === CatalogSourceType.HUGGING_FACE; diff --git a/clients/ui/frontend/src/concepts/modelCatalogSettings/utils.ts b/clients/ui/frontend/src/concepts/modelCatalogSettings/utils.ts new file mode 100644 index 0000000000..4318e168c0 --- /dev/null +++ b/clients/ui/frontend/src/concepts/modelCatalogSettings/utils.ts @@ -0,0 +1,31 @@ +import { CatalogSourceConfig } from '~/app/modelCatalogTypes'; +import { isHuggingFaceSource } from './const'; + +/** + * Checks if a catalog source has filters applied + * @param config - The catalog source configuration + * @returns true if the source has included or excluded models + */ +export const hasSourceFilters = (config: CatalogSourceConfig): boolean => { + const hasIncluded = config.includedModels && config.includedModels.length > 0; + const hasExcluded = config.excludedModels && config.excludedModels.length > 0; + return !!(hasIncluded || hasExcluded); +}; + +/** + * Gets the organization display value for a catalog source + * @param config - The catalog source configuration + * @param isDefault - Whether this is a default source + * @returns The organization name or '-' if not applicable + */ +export const getOrganizationDisplay = (config: CatalogSourceConfig, isDefault: boolean): string => { + if (isDefault) { + return '-'; + } + + if (isHuggingFaceSource(config)) { + return config.allowedOrganization || '-'; + } + + return '-'; +};
+ + {catalogSourceConfig.name} + + + + {organizationValue} + + + {hasFilters ? ( + + ) : ( + + )} + + + {CATALOG_SOURCE_TYPE_LABELS[catalogSourceConfig.type]} + + + {!isDefault && ( + handleEnableToggle(checked)} + /> + )} + {/* TODO: Status implementation */} + + + +