Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,18 @@ class RegisterAndStoreFields {
this.findSourceS3SecretAccessKeyInput().type(secretAccessKey);
}

/** Sets model type (required on register page). Uses Predictive by default. */
selectModelType(
optionName: 'Predictive Model' | 'Generative AI model (Example, LLM)' = 'Predictive Model',
) {
cy.get('#register-model-type-toggle').click();
cy.findByRole('option', { name: optionName }).click();
}

// Convenience method to fill all required fields for submission
fillAllRequiredFields() {
this.fillModelName('test-model');
this.selectModelType();
this.fillVersionName('v1.0.0');
this.fillJobName('my-transfer-job');
this.fillSourceEndpoint('https://s3.amazonaws.com');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ describe('Register and Store Fields - Credential Validation', () => {
it('Should have submit button disabled when S3 access key ID is missing', () => {
// Fill all fields except S3 access key ID
registerAndStoreFields.fillModelName('test-model');
registerAndStoreFields.selectModelType();
registerAndStoreFields.fillVersionName('v1.0.0');
registerAndStoreFields.fillJobName('my-transfer-job');
registerAndStoreFields.fillSourceEndpoint('https://s3.amazonaws.com');
Expand All @@ -349,6 +350,7 @@ describe('Register and Store Fields - Credential Validation', () => {
it('Should have submit button disabled when S3 secret access key is missing', () => {
// Fill all fields except S3 secret access key
registerAndStoreFields.fillModelName('test-model');
registerAndStoreFields.selectModelType();
registerAndStoreFields.fillVersionName('v1.0.0');
registerAndStoreFields.fillJobName('my-transfer-job');
registerAndStoreFields.fillSourceEndpoint('https://s3.amazonaws.com');
Expand All @@ -367,6 +369,7 @@ describe('Register and Store Fields - Credential Validation', () => {
it('Should have submit button disabled when OCI username is missing', () => {
// Fill all fields except OCI username
registerAndStoreFields.fillModelName('test-model');
registerAndStoreFields.selectModelType();
registerAndStoreFields.fillVersionName('v1.0.0');
registerAndStoreFields.fillJobName('my-transfer-job');
registerAndStoreFields.fillSourceEndpoint('https://s3.amazonaws.com');
Expand All @@ -385,6 +388,7 @@ describe('Register and Store Fields - Credential Validation', () => {
it('Should have submit button disabled when OCI password is missing', () => {
// Fill all fields except OCI password
registerAndStoreFields.fillModelName('test-model');
registerAndStoreFields.selectModelType();
registerAndStoreFields.fillVersionName('v1.0.0');
registerAndStoreFields.fillJobName('my-transfer-job');
registerAndStoreFields.fillSourceEndpoint('https://s3.amazonaws.com');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { CatalogArtifacts, CatalogModel, CatalogModelDetailsParams } from '~/app/modelCatalogTypes';
import { getCatalogModelDetailsRoute } from '~/app/routes/modelCatalog/catalogModelDetails';
import {
getCatalogModelTypePropertyForRegistration,
getModelArtifactUri,
getModelName,
} from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
Expand Down Expand Up @@ -81,7 +82,11 @@ const RegisterCatalogModelForm: React.FC<RegisterCatalogModelFormProps> = ({
jobResourceName: '',
modelRegistry: preferredModelRegistry.name,
namespace: '',
modelCustomProperties: { ...getLabelsFromCustomProperties(model?.customProperties), ...tasks },
modelCustomProperties: {
...getLabelsFromCustomProperties(model?.customProperties),
...tasks,
...getCatalogModelTypePropertyForRegistration(model?.customProperties),
},
versionCustomProperties: {
...model?.customProperties,
License: {
Expand Down Expand Up @@ -244,6 +249,7 @@ const RegisterCatalogModelForm: React.FC<RegisterCatalogModelFormProps> = ({
setData={setData}
hasModelNameError={hasModelNameError}
isModelNameDuplicate={isModelNameDuplicate}
isModelTypeReadOnly
/>
<RegistrationCommonFormSections
formData={formData}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
ModelCatalogTask,
ModelCatalogTensorType,
UseCaseOptionValue,
CatalogModelCustomPropertyKey,
ModelType,
} from '~/concepts/modelCatalog/const';
import {
CatalogSourceStatus,
Expand All @@ -34,6 +36,7 @@ import {
hasFiltersApplied,
getArchitecturesFromArtifacts,
getModelName,
getCatalogModelTypePropertyForRegistration,
getActiveSourceLabels,
} from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
import { mockCatalogModelArtifact } from '~/__mocks__/mockCatalogModelArtifactList';
Expand Down Expand Up @@ -1387,6 +1390,37 @@ describe('getModelName', () => {
});
});

describe('getCatalogModelTypePropertyForRegistration', () => {
it('returns model_type metadata when catalog has generative or predictive', () => {
expect(
getCatalogModelTypePropertyForRegistration({
[CatalogModelCustomPropertyKey.MODEL_TYPE]: {
metadataType: ModelRegistryMetadataType.STRING,
string_value: ModelType.GENERATIVE,
},
}),
).toEqual({
[CatalogModelCustomPropertyKey.MODEL_TYPE]: {
metadataType: ModelRegistryMetadataType.STRING,
string_value: ModelType.GENERATIVE,
},
});
});

it('returns empty object when model_type is absent or not a registerable value', () => {
expect(getCatalogModelTypePropertyForRegistration(undefined)).toEqual({});
expect(getCatalogModelTypePropertyForRegistration({})).toEqual({});
expect(
getCatalogModelTypePropertyForRegistration({
[CatalogModelCustomPropertyKey.MODEL_TYPE]: {
metadataType: ModelRegistryMetadataType.STRING,
string_value: ModelType.UNKNOWN,
},
}),
).toEqual({});
});
});

describe('getActiveSourceLabels', () => {
const createSource = (overrides: Partial<CatalogSource> = {}): CatalogSource => ({
id: 'source-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ import {
ModelType,
} from '~/concepts/modelCatalog/const';
import { isSourceStatusWithModels } from '~/concepts/modelCatalogSettings/const';
import { ModelRegistryMetadataType } from '~/app/types';
import { ModelRegistryCustomProperties, ModelRegistryMetadataType } from '~/app/types';
import {
buildCustomPropertiesWithModelType,
getModelTypeStoredValueFromCustomProperties,
} from '~/app/pages/modelRegistry/screens/RegisterModel/registerModelTypeUtils';

/**
* Prefix used by the backend for artifact-specific filter options.
Expand Down Expand Up @@ -789,3 +793,14 @@ export const formatModelTypeDisplay = (modelTypeRaw: string | null): string => {
// Fallback: capitalize whatever value we got
return capitalize(modelTypeRaw.trim());
};

/**
* Returns model registry customProperties entries to prefill `model_type` when registering
* from the catalog. Only generative/predictive are copied; unknown or missing values yield {}.
*/
export const getCatalogModelTypePropertyForRegistration = (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In here we can reuse reuse getModelTypeStoredValueFromCustomProperties from registerModelTypeUtils.ts , or even better - buildCustomPropertiesWithModelType

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reimplemented via getModelTypeStoredValueFromCustomProperties and buildCustomPropertiesWithModelType

customProperties?: ModelRegistryCustomProperties,
): ModelRegistryCustomProperties => {
const stored = getModelTypeStoredValueFromCustomProperties(customProperties);
return buildCustomPropertiesWithModelType(undefined, stored);
};
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const ModelVersionDetailsView: React.FC<ModelVersionDetailsViewProps> = ({
refresh={refresh}
isArchiveModel={isArchiveVersion}
isExpandable
modelTypeFallbackCustomProperties={mv.customProperties}
/>
</StackItem>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,30 @@ import {
DashboardDescriptionListGroup,
EditableLabelsDescriptionListGroup,
} from 'mod-arch-shared';
import { RegisteredModel } from '~/app/types';
import { ModelRegistryCustomProperties, RegisteredModel } from '~/app/types';
import ModelTimestamp from '~/app/pages/modelRegistry/screens/components/ModelTimestamp';
import ModelPropertiesExpandableSection from '~/app/pages/modelRegistry/screens/components/ModelPropertiesExpandableSection';
import { ModelRegistryContext } from '~/app/context/ModelRegistryContext';
import { getLabels, mergeUpdatedLabels } from '~/app/pages/modelRegistry/screens/utils';
import { EMPTY_CUSTOM_PROPERTY_VALUE } from '~/concepts/modelCatalog/const';
import { formatModelTypeDisplay } from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
import { getModelTypeRawStringFromCustomProperties } from '~/app/pages/modelRegistry/screens/RegisterModel/registerModelTypeUtils';

type ModelDetailsCardProps = {
registeredModel: RegisteredModel;
refresh: () => void;
isArchiveModel?: boolean;
isExpandable?: boolean;
/** If `model_type` is absent on the registered model, read from these (e.g. version custom properties). */
modelTypeFallbackCustomProperties?: ModelRegistryCustomProperties;
};

const ModelDetailsCard: React.FC<ModelDetailsCardProps> = ({
registeredModel: rm,
refresh,
isArchiveModel,
isExpandable,
modelTypeFallbackCustomProperties,
}) => {
const { apiState } = React.useContext(ModelRegistryContext);
const [isExpanded, setIsExpanded] = React.useState(false);
Expand Down Expand Up @@ -112,8 +117,17 @@ const ModelDetailsCard: React.FC<ModelDetailsCardProps> = ({
/>
);

const modelTypeRaw =
getModelTypeRawStringFromCustomProperties(rm.customProperties) ??
getModelTypeRawStringFromCustomProperties(modelTypeFallbackCustomProperties);

const infoSection = (
<>
<DashboardDescriptionListGroup title="Model type">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Taj010 one minor changes I noticed in the mockups is that we need move model ID field to the bottom of this listGroup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

<Content component="p" data-testid="registered-model-model-type">
{formatModelTypeDisplay(modelTypeRaw)}
</Content>
</DashboardDescriptionListGroup>
<DashboardDescriptionListGroup
title="Owner"
popover="The owner is the user who registered the model."
Expand All @@ -122,16 +136,6 @@ const ModelDetailsCard: React.FC<ModelDetailsCardProps> = ({
{rm.owner || EMPTY_CUSTOM_PROPERTY_VALUE}
</Content>
</DashboardDescriptionListGroup>
<DashboardDescriptionListGroup title="Model ID">
<ClipboardCopy
hoverTip="Copy"
clickTip="Copied"
variant="inline-compact"
data-testid="registered-model-id-clipboard-copy"
>
{rm.id}
</ClipboardCopy>
</DashboardDescriptionListGroup>
<DashboardDescriptionListGroup
isEmpty={!rm.lastUpdateTimeSinceEpoch}
contentWhenEmpty="Unknown"
Expand All @@ -146,6 +150,16 @@ const ModelDetailsCard: React.FC<ModelDetailsCardProps> = ({
>
<ModelTimestamp timeSinceEpoch={rm.createTimeSinceEpoch} />
</DashboardDescriptionListGroup>
<DashboardDescriptionListGroup title="Model ID">
<ClipboardCopy
hoverTip="Copy"
clickTip="Copied"
variant="inline-compact"
data-testid="registered-model-id-clipboard-copy"
>
{rm.id}
</ClipboardCopy>
</DashboardDescriptionListGroup>
</>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const RegisterModel: React.FC = () => {
registeredModels,
namespaceHasAccess,
isNamespaceAccessLoading,
{ requireModelType: true },
);

const handleSubmit = async () => {
Expand Down Expand Up @@ -160,6 +161,7 @@ const RegisterModel: React.FC = () => {
setData={setData}
hasModelNameError={hasModelNameError}
isModelNameDuplicate={isModelNameDuplicate}
isModelTypeRequired
/>
<RegistrationCommonFormSections
formData={formData}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,26 @@ import { UpdateObjectAtPropAndValue } from 'mod-arch-shared';
import FormFieldset from '~/app/pages/modelRegistry/screens/components/FormFieldset';
import FormSection from '~/app/pages/modelRegistry/components/pf-overrides/FormSection';
import { MR_CHARACTER_LIMIT } from './const';
import RegisterModelTypeField from './RegisterModelTypeField';
import { RegisterModelFormData } from './useRegisterModelData';

type RegisterModelDetailsFormSectionProp<D extends RegisterModelFormData> = {
formData: D;
setData: UpdateObjectAtPropAndValue<D>;
hasModelNameError: boolean;
isModelNameDuplicate?: boolean;
/** When true (MR register-from-registry), model type is required; submit stays disabled until set. */
isModelTypeRequired?: boolean;
/** When true (catalog → MR), model type is prefilled from catalog metadata and is not editable. */
isModelTypeReadOnly?: boolean;
};
const RegisterModelDetailsFormSection = <D extends RegisterModelFormData>({
formData,
setData,
hasModelNameError,
isModelNameDuplicate,
isModelTypeRequired = false,
isModelTypeReadOnly = false,
}: RegisterModelDetailsFormSectionProp<D>): React.ReactNode => {
const modelNameInput = (
<TextInput
Expand Down Expand Up @@ -75,6 +82,12 @@ const RegisterModelDetailsFormSection = <D extends RegisterModelFormData>({
</HelperText>
</FormHelperText>
</FormGroup>
<RegisterModelTypeField
modelCustomProperties={formData.modelCustomProperties}
onModelCustomPropertiesChange={(next) => setData('modelCustomProperties', next)}
isRequired={isModelTypeRequired}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looking at the mock ups both flow from model-registry and catalog - this model-type field is required. So just curios why we need this extra isRequired prop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

RegisterModelDetailsFormSection is shared: MR register passes isModelTypeRequired so the type field shows as required and matches isRegisterModelSubmitDisabled(..., { requireModelType: true }). Catalog register uses the same block but omits the prop and keeps requireModelType: false in isRegisterCatalogModelSubmitDisabled, so we only flag required in the MR path for now. If we want the catalog to match the mocks, we’d turn on the prop there and flip that submit check to true as well.

isReadOnly={isModelTypeReadOnly}
/>
</FormSection>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { FormGroup } from '@patternfly/react-core';
import React from 'react';
import { SimpleSelect } from 'mod-arch-shared';
import { SimpleSelectOption } from 'mod-arch-shared/dist/components/SimpleSelect';
import { ModelRegistryCustomProperties } from '~/app/types';
import { ModelType } from '~/concepts/modelCatalog/const';
import { formatModelTypeDisplay } from '~/app/pages/modelCatalog/utils/modelCatalogUtils';
import FormFieldset from '~/app/pages/modelRegistry/screens/components/FormFieldset';
import {
buildCustomPropertiesWithModelType,
getModelTypeStoredValueFromCustomProperties,
} from './registerModelTypeUtils';

const MODEL_TYPE_SELECT_OPTIONS: SimpleSelectOption[] = [
{
key: ModelType.GENERATIVE,
label: formatModelTypeDisplay(ModelType.GENERATIVE),
},
{
key: ModelType.PREDICTIVE,
label: formatModelTypeDisplay(ModelType.PREDICTIVE),
},
];

type RegisterModelTypeFieldProps = {
modelCustomProperties: ModelRegistryCustomProperties | undefined;
onModelCustomPropertiesChange: (next: ModelRegistryCustomProperties) => void;
isRequired?: boolean;
/** Catalog registration: type is catalog metadata (like URI), not user-editable. */
isReadOnly?: boolean;
};

const RegisterModelTypeField: React.FC<RegisterModelTypeFieldProps> = ({
modelCustomProperties,
onModelCustomPropertiesChange,
isRequired,
isReadOnly,
}) => {
const stored = getModelTypeStoredValueFromCustomProperties(modelCustomProperties);

const handleChange = (key: string) => {
if (key === ModelType.GENERATIVE || key === ModelType.PREDICTIVE) {
onModelCustomPropertiesChange(buildCustomPropertiesWithModelType(modelCustomProperties, key));
}
};

return (
<FormGroup label="Model type" isRequired={isRequired} fieldId="register-model-type">
<FormFieldset
field="Model type"
component={
<SimpleSelect
options={MODEL_TYPE_SELECT_OPTIONS}
value={stored ?? undefined}
onChange={handleChange}
placeholder="Select model type"
isFullWidth
isDisabled={isReadOnly}
dataTestId="register-model-type-select"
previewDescription={false}
popperProps={{ direction: 'down' }}
toggleProps={{ id: 'register-model-type-toggle' }}
/>
}
/>
</FormGroup>
);
};

export default RegisterModelTypeField;
Loading
Loading