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

Model Details and Model Version Details: add editable properties section, set up API patches for description, labels and properties #2770

Merged
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
44 changes: 22 additions & 22 deletions frontend/src/__mocks__/mockRegisteredModelsList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable camelcase */
import { RegisteredModelList } from '~/concepts/modelRegistry/types';
import { ModelRegistryMetadataType, RegisteredModelList } from '~/concepts/modelRegistry/types';
import { mockRegisteredModel } from './mockRegisteredModel';

export const mockRegisteredModelList = ({
Expand All @@ -14,27 +14,27 @@ export const mockRegisteredModelList = ({
'A machine learning model trained to detect fraudulent transactions in financial data',
customProperties: {
Financial: {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: 'non-empty',
},
'Financial data': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Fraud detection': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Test label': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Machine learning': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Next data to be overflow': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
},
Expand All @@ -43,23 +43,23 @@ export const mockRegisteredModelList = ({
name: 'Credit Scoring',
customProperties: {
'Credit Score Predictor': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Creditworthiness scoring system': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Default Risk Analyzer': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Portfolio Management': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Risk Assessment': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
},
Expand All @@ -70,44 +70,44 @@ export const mockRegisteredModelList = ({
'A machine learning model trained to detect fraudulent transactions in financial data',
customProperties: {
'Testing label': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
Financial: {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: 'non-empty',
},
'Financial data': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Fraud detection': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Long label data to be truncated abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc':
{
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Machine learning': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Next data to be overflow': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Label x': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Label y': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
'Label z': {
metadataType: 'MetadataStringValue',
metadataType: ModelRegistryMetadataType.STRING,
string_value: '',
},
},
Expand Down
11 changes: 2 additions & 9 deletions frontend/src/components/DashboardDescriptionListGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
DescriptionListTerm,
Split,
SplitItem,
Text,
} from '@patternfly/react-core';
import text from '@patternfly/react-styles/css/utilities/Text/text';
import { CheckIcon, PencilAltIcon, TimesIcon } from '@patternfly/react-icons';
Expand Down Expand Up @@ -70,7 +69,7 @@ const DashboardDescriptionListGroup: React.FC<DashboardDescriptionListGroupProps
</ActionListItem>
<ActionListItem>
<Button
data-testid={`discard-edit-button-${title} `}
data-testid={`discard-edit-button-${title}`}
aria-label={`Discard edits to ${title} `}
variant="plain"
onClick={onDiscardEditsClick}
Expand Down Expand Up @@ -100,13 +99,7 @@ const DashboardDescriptionListGroup: React.FC<DashboardDescriptionListGroupProps
<DescriptionListTerm>{title}</DescriptionListTerm>
)}
<DescriptionListDescription className={isEmpty && !isEditing ? text.disabledColor_100 : ''}>
{isEditing ? (
contentWhenEditing
) : isEmpty ? (
<Text style={{ color: '--pf-v5-global--Color--200' }}>{contentWhenEmpty}</Text>
) : (
children
)}
{isEditing ? contentWhenEditing : isEmpty ? contentWhenEmpty : children}
</DescriptionListDescription>
</DescriptionListGroup>
);
Expand Down
33 changes: 26 additions & 7 deletions frontend/src/components/EditableLabelsDescriptionListGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ type EditableTextDescriptionListGroupProps = Partial<
Pick<DashboardDescriptionListGroupProps, 'title' | 'contentWhenEmpty'>
> & {
labels: string[];
saveEditedLabels: (labels: string[]) => Promise<void>;
saveEditedLabels: (labels: string[]) => Promise<unknown>;
allExistingKeys?: string[];
};

const EditableLabelsDescriptionListGroup: React.FC<EditableTextDescriptionListGroupProps> = ({
title = 'Labels',
contentWhenEmpty = 'No labels',
labels,
saveEditedLabels,
allExistingKeys = labels,
}) => {
const [isEditing, setIsEditing] = React.useState(false);
const [unsavedLabels, setUnsavedLabels] = React.useState(labels);
Expand All @@ -54,10 +56,22 @@ const EditableLabelsDescriptionListGroup: React.FC<EditableTextDescriptionListGr
setUnsavedLabels([...unsavedLabels, text]);
};

// Don't allow a label that matches a non-label property key or another label (as they stand before saving)
// Note that this means if you remove a label and add it back before saving, that is valid
const reservedKeys = [
...allExistingKeys.filter((key) => !labels.includes(key)),
...unsavedLabels,
];

const [isAddLabelModalOpen, setIsAddLabelModalOpen] = React.useState(false);
const [addLabelInputValue, setAddLabelInputValue] = React.useState('');
const addLabelInputRef = React.useRef<HTMLInputElement>(null);
const addLabelInputTooLong = addLabelInputValue.length > 63;
let addLabelValidationError: string | null = null;
if (reservedKeys.includes(addLabelInputValue)) {
addLabelValidationError = 'Label must not match an existing label or property key';
} else if (addLabelInputValue.length > 63) {
addLabelValidationError = "Label text can't exceed 63 characters";
}

const toggleAddLabelModal = () => {
setAddLabelInputValue('');
Expand All @@ -68,7 +82,8 @@ const EditableLabelsDescriptionListGroup: React.FC<EditableTextDescriptionListGr
addLabelInputRef.current.focus();
}
}, [isAddLabelModalOpen]);
const addLabelModalSubmitDisabled = !addLabelInputValue || addLabelInputTooLong;

const addLabelModalSubmitDisabled = !addLabelInputValue || !!addLabelValidationError;
const submitAddLabelModal = (event?: React.FormEvent) => {
event?.preventDefault();
if (!addLabelModalSubmitDisabled) {
Expand Down Expand Up @@ -107,7 +122,11 @@ const EditableLabelsDescriptionListGroup: React.FC<EditableTextDescriptionListGr
editableProps={{ 'aria-label': `Editable label with text ${label}` }}
onClose={() => removeUnsavedLabel(label)}
closeBtnProps={{ isDisabled: isSavingEdits }}
onEditComplete={(_event, newText) => editUnsavedLabel(newText, index)}
onEditComplete={(_event, newText) => {
if (!reservedKeys.includes(newText) && newText.length <= 63) {
mturley marked this conversation as resolved.
Show resolved Hide resolved
editUnsavedLabel(newText, index);
}
}}
>
{label}
</Label>
Expand Down Expand Up @@ -172,13 +191,13 @@ const EditableLabelsDescriptionListGroup: React.FC<EditableTextDescriptionListGr
}
ref={addLabelInputRef}
isRequired
validated={addLabelInputTooLong ? 'error' : 'default'}
validated={addLabelValidationError ? 'error' : 'default'}
/>
{addLabelInputTooLong && (
{addLabelValidationError && (
<FormHelperText>
<HelperText>
<HelperTextItem icon={<ExclamationCircleIcon />} variant="error">
Label text can&apos;t exceed 63 characters
{addLabelValidationError}
</HelperTextItem>
</HelperText>
</FormHelperText>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/EditableTextDescriptionListGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type EditableTextDescriptionListGroupProps = Pick<
'title' | 'contentWhenEmpty'
> & {
value: string;
saveEditedValue: (value: string) => Promise<void>;
saveEditedValue: (value: string) => Promise<unknown>;
};

const EditableTextDescriptionListGroup: React.FC<EditableTextDescriptionListGroupProps> = ({
Expand Down Expand Up @@ -37,6 +37,8 @@ const EditableTextDescriptionListGroup: React.FC<EditableTextDescriptionListGrou
value={unsavedValue}
onChange={(_event, v) => setUnsavedValue(v)}
isDisabled={isSavingEdits}
rows={24}
resizeOrientation="vertical"
/>
}
onEditClick={() => {
Expand Down
53 changes: 52 additions & 1 deletion frontend/src/concepts/modelRegistry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,65 @@ export enum ModelArtifactState {
REFERENCE = 'REFERENCE',
}

export enum ModelRegistryMetadataType {
INT = 'MetadataIntValue',
DOUBLE = 'MetadataDoubleValue',
STRING = 'MetadataStringValue',
STRUCT = 'MetadataStructValue',
PROTO = 'MetadataProtoValue',
BOOL = 'MetadataBoolValue',
}

export type ModelRegistryCustomPropertyInt = {
metadataType: ModelRegistryMetadataType.INT;
int_value: string; // int64-formatted string
mturley marked this conversation as resolved.
Show resolved Hide resolved
};

export type ModelRegistryCustomPropertyDouble = {
metadataType: ModelRegistryMetadataType.DOUBLE;
double_value: number;
};

export type ModelRegistryCustomPropertyString = {
metadataType: ModelRegistryMetadataType.STRING;
string_value: string;
};

export type ModelRegistryCustomPropertyStruct = {
metadataType: ModelRegistryMetadataType.STRUCT;
struct_value: string; // Base64 encoded bytes for struct value
};

export type ModelRegistryCustomPropertyProto = {
metadataType: ModelRegistryMetadataType.PROTO;
type: string; // url describing proto value
proto_value: string; // Base64 encoded bytes for proto value
};

export type ModelRegistryCustomPropertyBool = {
metadataType: ModelRegistryMetadataType.BOOL;
bool_value: boolean;
};

export type ModelRegistryCustomProperty =
| ModelRegistryCustomPropertyInt
| ModelRegistryCustomPropertyDouble
| ModelRegistryCustomPropertyString
| ModelRegistryCustomPropertyStruct
| ModelRegistryCustomPropertyProto
| ModelRegistryCustomPropertyBool;

export type ModelRegistryCustomProperties = Record<string, ModelRegistryCustomProperty>;
export type ModelRegistryStringCustomProperties = Record<string, ModelRegistryCustomPropertyString>;

export type ModelRegistryBase = {
id: string;
name: string;
externalID?: string;
description?: string;
createTimeSinceEpoch?: string;
lastUpdateTimeSinceEpoch: string;
customProperties: Record<string, Record<string, string>>;
customProperties: ModelRegistryCustomProperties;
};

export type ModelArtifact = ModelRegistryBase & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ type GlobalModelVersionsTabProps = {
tab: ModelVersionsTabs;
registeredModel: RegisteredModel;
modelVersions: ModelVersion[];
refresh: () => void;
};

const GlobalModelVersionsTabs: React.FC<GlobalModelVersionsTabProps> = ({
tab,
registeredModel: rm,
modelVersions,
refresh,
}) => {
const navigate = useNavigate();
return (
Expand Down Expand Up @@ -44,7 +46,7 @@ const GlobalModelVersionsTabs: React.FC<GlobalModelVersionsTabProps> = ({
data-testid="model-details-tab"
>
<PageSection isFilled variant="light" data-testid="model-details-tab-content">
<ModelDetailsView registeredModel={rm} />
<ModelDetailsView registeredModel={rm} refresh={refresh} />
</PageSection>
</Tab>
</Tabs>
Expand Down
Loading
Loading