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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 33 additions & 18 deletions clients/ui/bff/internal/api/model_registry_settings_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,33 @@ func (app *App) GetAllModelRegistriesSettingsHandler(w http.ResponseWriter, r *h
Key: "ssl-secret-key",
}

lastTransitionTime, _ := time.Parse(time.RFC3339, "2024-03-22T09:30:02Z")
deletionTime := lastTransitionTime
registries := []models.ModelRegistryKind{
createSampleModelRegistry("model-registry", namespace, &sslRootCertificateConfigMap, nil),
createSampleModelRegistry("model-registry-dora", namespace, nil, &sslRootCertificateSecret),
createSampleModelRegistry("model-registry-bella", namespace, nil, nil),
createSampleModelRegistry("model-registry", namespace, &sslRootCertificateConfigMap, nil, nil, nil),
createSampleModelRegistry("model-registry-dora", namespace, nil, &sslRootCertificateSecret, nil, nil),
createSampleModelRegistry("model-registry-bella", namespace, nil, nil, nil, nil),
createSampleModelRegistry("model-registry-ready", namespace, nil, nil, nil,
[]models.Condition{
{LastTransitionTime: lastTransitionTime, Message: "Deployment for custom resource model-registry-ready is available", Reason: "Available", Status: "True", Type: "Available"},
}),
createSampleModelRegistry("model-registry-starting", namespace, nil, nil, nil,
[]models.Condition{
{LastTransitionTime: lastTransitionTime, Message: "Deployment for custom resource model-registry-starting was successfully created", Reason: "CreatedDeployment", Status: "True", Type: "Progressing"},
}),
createSampleModelRegistry("model-registry-stopping", namespace, nil, nil, &deletionTime,
[]models.Condition{
{LastTransitionTime: lastTransitionTime, Message: "Deployment for custom resource model-registry-stopping is available", Reason: "Available", Status: "True", Type: "Available"},
}),
createSampleModelRegistry("model-registry-degrading", namespace, nil, nil, nil,
[]models.Condition{
{LastTransitionTime: lastTransitionTime, Message: "Service is degrading", Reason: "Degraded", Status: "True", Type: "Degraded"},
}),
createSampleModelRegistry("model-registry-unavailable", namespace, nil, nil, nil,
[]models.Condition{
{LastTransitionTime: lastTransitionTime, Message: "Service is unavailable", Reason: "Unavailable", Status: "False", Type: "Available"},
{LastTransitionTime: lastTransitionTime, Message: "Istio resources are unavailable", Reason: "IstioUnavailable", Status: "False", Type: "IstioAvailable"},
}),
}

modelRegistryRes := ModelRegistrySettingsListEnvelope{
Expand All @@ -62,7 +85,7 @@ func (app *App) GetModelRegistrySettingsHandler(w http.ResponseWriter, r *http.R
}

modelId := ps.ByName(ModelRegistryId)
registry := createSampleModelRegistry(modelId, namespace, nil, nil)
registry := createSampleModelRegistry(modelId, namespace, nil, nil, nil, nil)

modelRegistryWithCreds := models.ModelRegistryAndCredentials{
ModelRegistry: registry,
Expand Down Expand Up @@ -102,7 +125,7 @@ func (app *App) CreateModelRegistrySettingsHandler(w http.ResponseWriter, r *htt

ctxLogger.Info("Creating model registry", "name", modelRegistryName)

registry := createSampleModelRegistry(modelRegistryName, namespace, nil, nil)
registry := createSampleModelRegistry(modelRegistryName, namespace, nil, nil, nil, nil)

modelRegistryRes := ModelRegistrySettingsEnvelope{
Data: registry,
Expand All @@ -125,7 +148,7 @@ func (app *App) UpdateModelRegistrySettingsHandler(w http.ResponseWriter, r *htt
}

modelId := ps.ByName(ModelRegistryId)
registry := createSampleModelRegistry(modelId, namespace, nil, nil)
registry := createSampleModelRegistry(modelId, namespace, nil, nil, nil, nil)

modelRegistryRes := ModelRegistrySettingsEnvelope{
Data: registry,
Expand All @@ -148,7 +171,7 @@ func (app *App) DeleteModelRegistrySettingsHandler(w http.ResponseWriter, r *htt

// This is a temporary fix to handle frontend error (as it is expecting ModelRegistryKind response) until we have a real implementation
modelId := ps.ByName(ModelRegistryId)
registry := createSampleModelRegistry(modelId, namespace, nil, nil)
registry := createSampleModelRegistry(modelId, namespace, nil, nil, nil, nil)

modelRegistryRes := ModelRegistrySettingsEnvelope{
Data: registry,
Expand All @@ -161,10 +184,9 @@ func (app *App) DeleteModelRegistrySettingsHandler(w http.ResponseWriter, r *htt
}

// This function is a temporary function to create a sample model registry kind until we have a real implementation
func createSampleModelRegistry(name string, namespace string, SSLRootCertificateConfigMap *models.Entry, SSLRootCertificateSecret *models.Entry) models.ModelRegistryKind {
func createSampleModelRegistry(name string, namespace string, SSLRootCertificateConfigMap *models.Entry, SSLRootCertificateSecret *models.Entry, deletionTimestamp *time.Time, conditions []models.Condition) models.ModelRegistryKind {

creationTime, _ := time.Parse(time.RFC3339, "2024-03-14T08:01:42Z")
lastTransitionTime, _ := time.Parse(time.RFC3339, "2024-03-22T09:30:02Z")

return models.ModelRegistryKind{
APIVersion: "modelregistry.io/v1alpha1",
Expand All @@ -173,6 +195,7 @@ func createSampleModelRegistry(name string, namespace string, SSLRootCertificate
Name: name,
Namespace: namespace,
CreationTimestamp: creationTime,
DeletionTimestamp: deletionTimestamp,
Annotations: map[string]string{},
},
Spec: models.ModelRegistrySpec{
Expand Down Expand Up @@ -205,15 +228,7 @@ func createSampleModelRegistry(name string, namespace string, SSLRootCertificate
},
},
Status: models.Status{
Conditions: []models.Condition{
{
LastTransitionTime: lastTransitionTime,
Message: "Deployment for custom resource " + name + " was successfully created",
Reason: "CreatedDeployment",
Status: "True",
Type: "Progressing",
},
},
Conditions: conditions,
},
}
}
1 change: 1 addition & 0 deletions clients/ui/bff/internal/models/model_registry_kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Metadata struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ describe('Model transfer jobs', () => {
failedRow.find().findByTestId('job-status').should('contain.text', 'Failed');

const cancelledRow = modelTransferJobsPage.getRow('job-cancelled');
cancelledRow.find().findByTestId('job-status').should('contain.text', 'Cancelled');
cancelledRow.find().findByTestId('job-status').should('contain.text', 'Canceled');

completedRow.find().findByTestId('job-status').click();
cy.findByTestId('transfer-job-status-modal').should('be.visible');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { waitFor } from '@testing-library/react';
import { act, waitFor } from '@testing-library/react';
import { useFetchState, POLL_INTERVAL } from 'mod-arch-core';
import useModelTransferJobs from '~/app/hooks/useModelTransferJobs';
import { useModelRegistryAPI } from '~/app/hooks/useModelRegistryAPI';
Expand Down Expand Up @@ -129,10 +129,9 @@ describe('useModelTransferJobs', () => {
// toggle hasActiveJobs to true and cause a re-render with refreshRate = POLL_INTERVAL.
const capturedCallback = getCapturedCallback();
expect(capturedCallback).toBeDefined();
await capturedCallback?.({});

// Wait for the hook to re-render after state update
await renderResult.waitForNextUpdate();
await act(async () => {
await capturedCallback?.({});
});

// The latest call to useFetchState should have refreshRate set to POLL_INTERVAL
const lastCallOptions = optionsCalls[optionsCalls.length - 1];
Expand Down Expand Up @@ -184,7 +183,9 @@ describe('useModelTransferJobs', () => {
// so refreshRate should remain undefined (no need to wait for another update).
const capturedCallback = getCapturedCallback();
expect(capturedCallback).toBeDefined();
await capturedCallback?.({});
await act(async () => {
await capturedCallback?.({});
});

const lastCallOptions = optionsCalls[optionsCalls.length - 1];
expect(lastCallOptions.refreshRate).toBeUndefined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const CatalogSourceStatus: React.FC<CatalogSourceStatusProps> = ({ catalogSource
const startingOrUnknownLabel = (
<Label
color="grey"
variant="outline"
icon={<InProgressIcon />}
data-testid={`source-status-${catalogSourcesLoadError ? 'unknown' : 'starting'}-${catalogSourceConfig.id}`}
>
Expand All @@ -53,7 +54,11 @@ const CatalogSourceStatus: React.FC<CatalogSourceStatusProps> = ({ catalogSource
switch (matchingSource.status) {
case CatalogSourceStatusEnum.AVAILABLE:
return (
<Label status="success" data-testid={`source-status-connected-${catalogSourceConfig.id}`}>
<Label
status="success"
variant="outline"
data-testid={`source-status-connected-${catalogSourceConfig.id}`}
>
Connected
</Label>
);
Expand All @@ -65,7 +70,11 @@ const CatalogSourceStatus: React.FC<CatalogSourceStatusProps> = ({ catalogSource
<>
<Stack hasGutter>
<StackItem>
<Label status="danger" data-testid={`source-status-failed-${catalogSourceConfig.id}`}>
<Label
status="danger"
variant="outline"
data-testid={`source-status-failed-${catalogSourceConfig.id}`}
>
Failed
</Label>
</StackItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const CatalogSourceStatusErrorModal: React.FC<CatalogSourceStatusErrorModalProps
<Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem>Source status</FlexItem>
<FlexItem>
<Label color="red" icon={<ExclamationCircleIcon />}>
<Label status="danger" variant="outline" icon={<ExclamationCircleIcon />}>
Failed
</Label>
</FlexItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import {
ModelCatalogSettingsContext,
ModelCatalogSettingsContextType,
} from '~/app/context/modelCatalogSettings/ModelCatalogSettingsContext';
import { CatalogSourceConfig, CatalogSourceType } from '~/app/modelCatalogTypes';
import CatalogSourceStatus from '~/app/pages/modelCatalogSettings/components/CatalogSourceStatus';

const mockConfig: CatalogSourceConfig = {
id: 'test-source',
name: 'Test Source',
type: CatalogSourceType.YAML,
enabled: true,
};

const defaultPagination = { size: 0, pageSize: 10, nextPageToken: '' };

const renderWithContext = (
config: CatalogSourceConfig,
contextOverrides: Partial<ModelCatalogSettingsContextType>,
) => {
const defaultContext: ModelCatalogSettingsContextType = {
apiState: {
apiAvailable: false,
api: null as unknown as ModelCatalogSettingsContextType['apiState']['api'],
},
refreshAPIState: jest.fn(),
catalogSourceConfigs: null,
catalogSourceConfigsLoaded: false,
catalogSourceConfigsLoadError: undefined,
refreshCatalogSourceConfigs: jest.fn(),
catalogSources: null,
catalogSourcesLoaded: true,
catalogSourcesLoadError: undefined,
refreshCatalogSources: jest.fn(),
...contextOverrides,
};

return render(
<ModelCatalogSettingsContext.Provider value={defaultContext}>
<CatalogSourceStatus catalogSourceConfig={config} />
</ModelCatalogSettingsContext.Provider>,
);
};

describe('CatalogSourceStatus', () => {
it('renders "Connected" label with outline variant', () => {
renderWithContext(mockConfig, {
catalogSources: {
...defaultPagination,
items: [{ id: 'test-source', name: 'Test', labels: [], status: 'available' }],
},
catalogSourcesLoaded: true,
});

const label = screen.getByTestId('source-status-connected-test-source');
expect(screen.getByText('Connected')).toBeVisible();
expect(label.className).toMatch(/outline/);
expect(label.className).not.toMatch(/filled/);
});

it('renders "Failed" label with outline variant', () => {
renderWithContext(mockConfig, {
catalogSources: {
...defaultPagination,
items: [
{
id: 'test-source',
name: 'Test',
labels: [],
status: 'error',
error: 'Connection refused',
},
],
},
catalogSourcesLoaded: true,
});

const label = screen.getByTestId('source-status-failed-test-source');
expect(screen.getByText('Failed')).toBeVisible();
expect(label.className).toMatch(/outline/);
expect(label.className).not.toMatch(/filled/);
});

it('renders "Starting" label with outline variant when source has no status', () => {
renderWithContext(mockConfig, {
catalogSources: {
...defaultPagination,
items: [{ id: 'test-source', name: 'Test', labels: [] }],
},
catalogSourcesLoaded: true,
});

const label = screen.getByTestId('source-status-starting-test-source');
expect(screen.getByText('Starting')).toBeVisible();
expect(label.className).toMatch(/outline/);
});

it('renders "Unknown" label with outline variant when there is a load error', () => {
renderWithContext(mockConfig, {
catalogSources: null,
catalogSourcesLoaded: true,
catalogSourcesLoadError: new Error('API error'),
});

const label = screen.getByTestId('source-status-unknown-test-source');
expect(screen.getByText('Unknown')).toBeVisible();
expect(label.className).toMatch(/outline/);
});

it('renders "-" for default sources', () => {
renderWithContext({ ...mockConfig, isDefault: true }, { catalogSourcesLoaded: true });
expect(screen.getByText('-')).toBeVisible();
});

it('renders "-" for disabled sources', () => {
renderWithContext({ ...mockConfig, enabled: false }, { catalogSourcesLoaded: true });
expect(screen.getByText('-')).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ const ModelTransferJobStatusModal: React.FC<ModelTransferJobStatusModalProps> =
<Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem>{getModalTitle(job.uploadIntent)}</FlexItem>
<FlexItem>
<Label color={statusInfo.color} icon={statusInfo.icon}>
<Label
color={statusInfo.color}
status={statusInfo.status}
icon={statusInfo.icon}
variant="outline"
>
{statusInfo.label}
</Label>
</FlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,21 @@ export const getStatusLabel = (
status: ModelTransferJobStatus,
): {
label: string;
color: React.ComponentProps<typeof Label>['color'];
color?: React.ComponentProps<typeof Label>['color'];
status?: React.ComponentProps<typeof Label>['status'];
icon: React.ReactNode;
} => {
switch (status) {
case ModelTransferJobStatus.COMPLETED:
return { label: 'Complete', color: 'green', icon: <CheckCircleIcon /> };
return { label: 'Complete', status: 'success', icon: <CheckCircleIcon /> };
case ModelTransferJobStatus.RUNNING:
return { label: 'Running', color: 'blue', icon: <InProgressIcon /> };
case ModelTransferJobStatus.PENDING:
return { label: 'Pending', color: 'grey', icon: <PendingIcon /> };
return { label: 'Pending', color: 'purple', icon: <PendingIcon /> };
case ModelTransferJobStatus.FAILED:
return { label: 'Failed', color: 'red', icon: <ExclamationCircleIcon /> };
return { label: 'Failed', status: 'danger', icon: <ExclamationCircleIcon /> };
case ModelTransferJobStatus.CANCELLED:
return { label: 'Cancelled', color: 'grey', icon: <BanIcon /> };
return { label: 'Canceled', color: 'grey', icon: <BanIcon /> };
default:
return { label: status, color: 'grey', icon: null };
}
Expand Down Expand Up @@ -134,8 +135,10 @@ const ModelTransferJobTableRow: React.FC<ModelTransferJobTableRowProps> = ({
<FlexItem>
<Label
color={statusInfo.color}
status={statusInfo.status}
icon={statusInfo.icon}
data-testid="job-status"
variant="filled"
isClickable
onClick={() => setIsStatusModalOpen(true)}
>
Expand Down
Loading
Loading