Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sorting on models page by Last Modified is reflecting changes in versions as well #3597

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion frontend/src/api/modelRegistry/__tests__/custom.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe('createModelVersionForRegisteredModel', () => {
state: ModelState.LIVE,
customProperties: {},
}),
).toBe(mockResultPromise);
).toEqual(mockResultPromise);
expect(proxyCREATEMock).toHaveBeenCalledTimes(1);
expect(proxyCREATEMock).toHaveBeenCalledWith(
'hostPath',
Expand Down
25 changes: 17 additions & 8 deletions frontend/src/api/modelRegistry/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {
RegisteredModelList,
RegisteredModel,
} from '~/concepts/modelRegistry/types';
import { MODEL_REGISTRY_API_VERSION } from '~/concepts/modelRegistry/const';
import { bumpRegisteredModelTimestamp } from '~/concepts/modelRegistry/utils/updateTimestamps';
import { proxyCREATE, proxyGET, proxyPATCH } from '~/api/proxyUtils';
import { K8sAPIOptions } from '~/k8sTypes';
import { MODEL_REGISTRY_API_VERSION } from '~/concepts/modelRegistry/const';
import { handleModelRegistryFailures } from './errorUtils';

export const createRegisteredModel =
Expand Down Expand Up @@ -41,12 +42,12 @@ export const createModelVersion =
);
export const createModelVersionForRegisteredModel =
(hostpath: string) =>
(
async (
opts: K8sAPIOptions,
registeredModelId: string,
data: CreateModelVersionData,
): Promise<ModelVersion> =>
handleModelRegistryFailures(
): Promise<ModelVersion> => {
const newVersion = await handleModelRegistryFailures<ModelVersion>(
proxyCREATE(
hostpath,
`/api/model_registry/${MODEL_REGISTRY_API_VERSION}/registered_models/${registeredModelId}/versions`,
Expand All @@ -56,6 +57,14 @@ export const createModelVersionForRegisteredModel =
),
);

await bumpRegisteredModelTimestamp(
{ patchRegisteredModel: patchRegisteredModel(hostpath) },
registeredModelId,
);

return newVersion;
};

export const createModelArtifact =
(hostPath: string) =>
(opts: K8sAPIOptions, data: CreateModelArtifactData): Promise<ModelArtifact> =>
Expand Down Expand Up @@ -194,7 +203,7 @@ export const patchRegisteredModel =
data: Partial<RegisteredModel>,
registeredModelId: string,
): Promise<RegisteredModel> =>
handleModelRegistryFailures(
handleModelRegistryFailures<RegisteredModel>(
proxyPATCH(
hostPath,
`/api/model_registry/${MODEL_REGISTRY_API_VERSION}/registered_models/${registeredModelId}`,
Expand All @@ -208,12 +217,12 @@ export const patchModelVersion =
(
opts: K8sAPIOptions,
data: Partial<ModelVersion>,
modelversionId: string,
modelVersionId: string,
): Promise<ModelVersion> =>
handleModelRegistryFailures(
handleModelRegistryFailures<ModelVersion>(
proxyPATCH(
hostPath,
`/api/model_registry/${MODEL_REGISTRY_API_VERSION}/model_versions/${modelversionId}`,
`/api/model_registry/${MODEL_REGISTRY_API_VERSION}/model_versions/${modelVersionId}`,
data,
opts,
),
Expand Down
70 changes: 70 additions & 0 deletions frontend/src/concepts/modelRegistry/utils/updateTimestamps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
ModelRegistryAPIs,
ModelState,
ModelRegistryMetadataType,
} from '~/concepts/modelRegistry/types';

type MinimalModelRegistryAPI = Pick<ModelRegistryAPIs, 'patchRegisteredModel'>;

export const bumpModelVersionTimestamp = async (
api: ModelRegistryAPIs,
modelVersionId: string,
): Promise<void> => {
if (!modelVersionId) {
throw new Error('Model version ID is required');
}

try {
await api.patchModelVersion({}, { state: ModelState.LIVE }, modelVersionId);
} catch (error) {
throw new Error(
`Failed to update model version timestamp: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
};

export const bumpRegisteredModelTimestamp = async (
api: MinimalModelRegistryAPI,
registeredModelId: string,
): Promise<void> => {
if (!registeredModelId) {
throw new Error('Registered model ID is required');
}

try {
const currentTime = new Date().toISOString();
await api.patchRegisteredModel(
{},
{
state: ModelState.LIVE,
customProperties: {
_lastModified: {
metadataType: ModelRegistryMetadataType.STRING,
// eslint-disable-next-line camelcase
string_value: currentTime,
},
},
Comment on lines +42 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up with this customProperties piece? I don't think it should be necessary, and it'll show up in the properties table in the UI

Screenshot 2024-12-19 at 4 26 57 PM

I think all we need is the { state } part.

Also... and this part is more annoying... I wonder if it's ok to just pass ModelState.LIVE here, technically if you called this on an archived model it would un-archive it. but since the UI also prevents editing archived stuff I think we're ok. We may just want a comment explaining that here.

Copy link
Contributor

@mturley mturley Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking more - if the _lastModified addition here was because the { state } alone wasn't enough to automatically bump the version, that's an issue we should raise again with the backend team. If we get blocked on this and it looks like it won't make 2.17 we could remove it from the strat scope. Or if we want a temporary workaround we can leave this, but we should maybe filter it out of the properties list so it doesn't look confusing, and open a followup issue we link in comments here and where that filter is added.

},
registeredModelId,
);
} catch (error) {
throw new Error(
`Failed to update registered model timestamp: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
};

export const bumpBothTimestamps = async (
api: ModelRegistryAPIs,
modelVersionId: string,
registeredModelId: string,
): Promise<void> => {
await Promise.all([
bumpModelVersionTimestamp(api, modelVersionId),
bumpRegisteredModelTimestamp(api, registeredModelId),
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { ModelRegistryContext } from '~/concepts/modelRegistry/context/ModelRegi
import ModelTimestamp from '~/pages/modelRegistry/screens/components/ModelTimestamp';
import { uriToObjectStorageFields } from '~/concepts/modelRegistry/utils';
import InlineTruncatedClipboardCopy from '~/components/InlineTruncatedClipboardCopy';
import {
bumpBothTimestamps,
bumpRegisteredModelTimestamp,
} from '~/concepts/modelRegistry/utils/updateTimestamps';

type ModelVersionDetailsViewProps = {
modelVersion: ModelVersion;
Expand All @@ -47,6 +51,28 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
</Bullseye>
);
}
const handleVersionUpdate = async (updatePromise: Promise<unknown>): Promise<void> => {
await updatePromise;
YuliaKrimerman marked this conversation as resolved.
Show resolved Hide resolved

if (!mv.registeredModelId) {
return;
}

await bumpRegisteredModelTimestamp(apiState.api, mv.registeredModelId);
refresh();
};

const handleArtifactUpdate = async (updatePromise: Promise<unknown>): Promise<void> => {
try {
await updatePromise;
await bumpBothTimestamps(apiState.api, mv.id, mv.registeredModelId);
refreshModelArtifacts();
} catch (error) {
throw new Error(
`Failed to update artifact: ${error instanceof Error ? error.message : String(error)}`,
);
}
};

return (
<Flex
Expand All @@ -64,15 +90,7 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
contentWhenEmpty="No description"
value={mv.description || ''}
saveEditedValue={(value) =>
apiState.api
.patchModelVersion(
{},
{
description: value,
},
mv.id,
)
.then(refresh)
handleVersionUpdate(apiState.api.patchModelVersion({}, { description: value }, mv.id))
}
/>
<EditableLabelsDescriptionListGroup
Expand All @@ -82,15 +100,13 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
title="Labels"
contentWhenEmpty="No labels"
onLabelsChange={(editedLabels) =>
apiState.api
.patchModelVersion(
handleVersionUpdate(
apiState.api.patchModelVersion(
{},
{
customProperties: mergeUpdatedLabels(mv.customProperties, editedLabels),
},
{ customProperties: mergeUpdatedLabels(mv.customProperties, editedLabels) },
mv.id,
)
.then(refresh)
),
)
}
data-testid="model-version-labels"
/>
Expand Down Expand Up @@ -195,11 +211,13 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
isArchive={isArchiveVersion}
value={modelArtifact?.modelFormatName || ''}
saveEditedValue={(value) =>
apiState.api
.patchModelArtifact({}, { modelFormatName: value }, modelArtifact?.id || '')
.then(() => {
refreshModelArtifacts();
})
handleArtifactUpdate(
apiState.api.patchModelArtifact(
{},
{ modelFormatName: value },
modelArtifact?.id || '',
),
)
}
title="Model Format"
contentWhenEmpty="No model format specified"
Expand All @@ -210,15 +228,13 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
value={modelArtifact?.modelFormatVersion || ''}
isArchive={isArchiveVersion}
saveEditedValue={(newVersion) =>
apiState.api
.patchModelArtifact(
handleArtifactUpdate(
apiState.api.patchModelArtifact(
{},
{ modelFormatVersion: newVersion },
modelArtifact?.id || '',
)
.then(() => {
refreshModelArtifacts();
})
),
)
}
title="Version"
contentWhenEmpty="No source model format version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ModelVersionsTable: React.FC<ModelVersionsTableProps> = ({
!!inferenceServices.data.some(
(s) => s.metadata.labels?.[KnownLabels.MODEL_VERSION_ID] === mvId,
);

return (
<Table
data-testid="model-versions-table"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,15 @@ const ModelVersionsTableRow: React.FC<ModelVersionsTableRowProps> = ({
) : null}
{isDeployModalOpen ? (
<DeployRegisteredModelModal
onSubmit={() =>
onSubmit={() => {
navigate(
modelVersionDeploymentsUrl(
mv.id,
mv.registeredModelId,
preferredModelRegistry?.metadata.name,
),
)
}
);
}}
onCancel={() => setIsDeployModalOpen(false)}
modelVersion={mv}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Alert, Button, Form, FormSection, Spinner } from '@patternfly/react-core';
import { Modal } from '@patternfly/react-core/deprecated';
import { ModelVersion } from '~/concepts/modelRegistry/types';
import { ModelVersion, ModelState } from '~/concepts/modelRegistry/types';
import { ProjectKind } from '~/k8sTypes';
import useProjectErrorForRegisteredModel from '~/pages/modelRegistry/screens/RegisteredModels/useProjectErrorForRegisteredModel';
import ProjectSelector from '~/pages/modelServing/screens/projects/InferenceServiceModal/ProjectSelector';
Expand All @@ -11,7 +11,10 @@ import { getProjectModelServingPlatform } from '~/pages/modelServing/screens/pro
import { ServingRuntimePlatform } from '~/types';
import ManageInferenceServiceModal from '~/pages/modelServing/screens/projects/InferenceServiceModal/ManageInferenceServiceModal';
import useRegisteredModelDeployInfo from '~/pages/modelRegistry/screens/RegisteredModels/useRegisteredModelDeployInfo';
import { ModelRegistryContext } from '~/concepts/modelRegistry/context/ModelRegistryContext';
import {
ModelRegistryContext,
useModelRegistryAPI,
} from '~/concepts/modelRegistry/context/ModelRegistryContext';
import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext';
import { getKServeTemplates } from '~/pages/modelServing/customServingRuntimes/utils';
import useDataConnections from '~/pages/projects/screens/detail/data-connections/useDataConnections';
Expand All @@ -33,6 +36,7 @@ const DeployRegisteredModelModal: React.FC<DeployRegisteredModelModalProps> = ({
servingRuntimeTemplateDisablement: { data: templateDisablement },
} = React.useContext(ModelRegistryContext);
const { preferredModelRegistry } = React.useContext(ModelRegistrySelectorContext);
const modelRegistryApi = useModelRegistryAPI();

const [selectedProject, setSelectedProject] = React.useState<ProjectKind | null>(null);
const servingPlatformStatuses = useServingPlatformStatuses();
Expand All @@ -51,16 +55,35 @@ const DeployRegisteredModelModal: React.FC<DeployRegisteredModelModalProps> = ({
error: deployInfoError,
} = useRegisteredModelDeployInfo(modelVersion);

const handleSubmit = React.useCallback(async () => {
if (!modelVersion.registeredModelId) {
return;
}

try {
await Promise.all([
modelRegistryApi.api.patchModelVersion({}, { state: ModelState.LIVE }, modelVersion.id),
modelRegistryApi.api.patchRegisteredModel(
{},
{ state: ModelState.LIVE },
modelVersion.registeredModelId,
),
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still have direct patch calls here where I think you need to be using bumpBothTimestamps instead to make sure the _lastModified property workaround gets applied

onSubmit?.();
} catch (submitError) {
throw new Error('Failed to update timestamps after deployment');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may have missed this before - throwing an error here doesn't show it to the user, it just ends up in the browser console since there's nothing catching it that renders it (like in some places where we have errors stored in state and use them to render an Alert). That might be okay for now... we actually aren't catching errors with a lot of the patches the MR edit controls do...... 🤦 that's probably worth a tech debt issue. I'll make a note of it.

}
}, [modelRegistryApi, modelVersion.id, modelVersion.registeredModelId, onSubmit]);

const onClose = React.useCallback(
(submit: boolean) => {
if (submit) {
onSubmit?.();
handleSubmit();
}

setSelectedProject(null);
onCancel();
},
[onCancel, onSubmit],
[handleSubmit, onCancel],
);

const projectSection = (
Expand Down Expand Up @@ -101,7 +124,7 @@ const DeployRegisteredModelModal: React.FC<DeployRegisteredModelModalProps> = ({
isOpen
onClose={() => onClose(false)}
actions={[
<Button key="deploy" variant="primary" isDisabled>
<Button key="deploy" variant="primary" onClick={handleSubmit}>
Deploy
</Button>,
<Button key="cancel" variant="link" onClick={() => onClose(false)}>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/pages/modelRegistry/screens/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export const getProperties = <T extends ModelRegistryCustomProperties>(
): ModelRegistryStringCustomProperties => {
const initial: ModelRegistryStringCustomProperties = {};
return Object.keys(customProperties).reduce((acc, key) => {
if (key === '_lastModified') {
return acc;
}

const prop = customProperties[key];
if (prop.metadataType === ModelRegistryMetadataType.STRING && prop.string_value !== '') {
return { ...acc, [key]: prop };
Expand Down
Loading