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

Feature/model registry #388

Closed
wants to merge 68 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
241167f
Revert "feat: remove model registry related code and dependencies (#63)"
ruanyl Jan 31, 2023
aeaa481
Feat add show my models in owner filter (#29)
wanglam Jan 30, 2023
bf4f2dd
bring in-app navigation bar back (#72)
ruanyl Jan 31, 2023
6b5696a
feat: add more actions in model list (#77)
raintygao Feb 1, 2023
f2a1dac
populate register form with existing model version (#82)
raintygao Feb 2, 2023
e3f35df
Feature/add model upload logic (#83)
wanglam Feb 8, 2023
4a5ce67
feat: update register model form ui according to the new design (#85)
ruanyl Feb 8, 2023
5453bf5
update help button location and flyout content according to updated d…
raintygao Feb 9, 2023
c36704e
feat: add multiple validation rules on tag field (#93)
ruanyl Feb 13, 2023
d2a566c
feat: align ui with the latest design changes (#95)
ruanyl Feb 13, 2023
4947f6f
add metrics validation (#97)
raintygao Feb 14, 2023
228610b
feat: add register form submission footer (#99)
ruanyl Feb 14, 2023
3adf3dd
feat: show notifications if form submit success or fail (#110)
ruanyl Feb 20, 2023
54348eb
feat: add model_register_button to model_list (#80)
xyinshen Feb 21, 2023
dce879d
feat: add upload callout (#113)
wanglam Feb 22, 2023
0fd850e
fix: update-register-form-hearder-descriptions (#114)
xyinshen Feb 22, 2023
6aae405
feat: upload file after register form submitted (#117)
ruanyl Feb 22, 2023
944fa98
feat: update artifact file validation rules (#118)
ruanyl Feb 24, 2023
9eefbc2
feat: display notification when upload model by URL (#126)
ruanyl Feb 27, 2023
89bc6cf
Feature/add model name unique verification (#129)
wanglam Mar 7, 2023
f415373
Feature/rename annotation and remove model details (#133)
wanglam Mar 7, 2023
de06d36
Feature/fetch pre trained model list (#131)
wanglam Mar 8, 2023
93e45c8
Merge remote-tracking branch 'base/2.x' into feature/model-registry
ruanyl Mar 9, 2023
95d9d59
fix: revert legacy pagination methods
ruanyl Mar 9, 2023
f25e4fe
feat: init model group page (#134)
ruanyl Mar 10, 2023
00a2431
Featuer/fill pre trained model data to register form (#135)
wanglam Mar 10, 2023
2735866
Feature file version title and configuration description (#130)
xyinshen Mar 10, 2023
7fe1dc2
fix: tweak test mocks (#137)
ruanyl Mar 13, 2023
cd2700b
Feature update description max width 725 (#140)
xyinshen Mar 13, 2023
73962ce
feat: disallow user to type if text exceed max length (#138)
ruanyl Mar 14, 2023
49923ec
Feature/update model register tags logic (#142)
wanglam Mar 16, 2023
ebf6e29
feat: add form error call-out (#141)
ruanyl Mar 16, 2023
eb3ac0f
feat: add model file format select (#143)
ruanyl Mar 17, 2023
6c45264
feat: tweaks form section titles per new design (#144)
ruanyl Mar 20, 2023
dd99925
Merge remote-tracking branch 'base/2.x' into feature/model-registry
ruanyl Mar 21, 2023
cc77e89
Feature change register form max width to 1000px and make it centered…
xyinshen Mar 22, 2023
f752ac9
test: initiate the use of MSW for API mocking (#147)
ruanyl Mar 24, 2023
603d536
Feature/add model version detail mock page (#153)
wanglam Apr 6, 2023
ef3639a
feat: support for adding tag types (#161)
ruanyl Apr 19, 2023
6cf03ab
Feature/replace model list stage filter with deployment toggle (#163)
wanglam Apr 20, 2023
9b23a3e
Feature/update tag filter (#162)
wanglam Apr 23, 2023
12bb019
Feature/update model detail page layout (#166)
wanglam Apr 26, 2023
25d1c5e
Feature/update global breadcrumbs (#164)
wanglam Apr 27, 2023
7c5cdf3
Merge branch '2.7' into feature/model-registry
ruanyl May 4, 2023
9e173d5
Feature/add model loading empty failed screen for model list (#165)
wanglam May 6, 2023
a11b519
Feature/add versions table in model group detail (#170)
wanglam May 9, 2023
94b30ef
Feature/add id column for model versions table (#177)
wanglam May 11, 2023
890f14b
Version details page mockup (#179)
ruanyl May 12, 2023
6dd89e6
Feature/add details tab content in model group page (#176)
wanglam May 12, 2023
1628954
feat(ui): version information edit component (#180)
ruanyl May 17, 2023
4e20dfb
Feature/add tags tab content model group page (#181)
wanglam May 18, 2023
4ed49e5
feat: version tag edit (#182)
ruanyl May 22, 2023
bef804b
feat(ui): artifact and configuration edit (#187)
ruanyl May 23, 2023
9f0b3ce
Feature/add model version loading empty error screens (#185)
wanglam May 24, 2023
6424c8e
Feature/add deploy confirmation modal (#186)
wanglam May 24, 2023
4161537
Feature/create model group before register (#192)
wanglam May 26, 2023
7b41f36
Feature/jump to model detail page with correct (#193)
wanglam May 26, 2023
cd08129
Feature/update model list with model group (#197)
wanglam May 30, 2023
1ead540
feat: deploy and undeploy api integration (#198)
ruanyl May 31, 2023
763797b
fix: map model_group_id to model_id (#207)
ruanyl Jun 12, 2023
dcb1fe4
Feature/add model version delete modal (#199)
wanglam Jun 12, 2023
1ab5f97
Merge 2.8 to model registry (#215)
wanglam Jun 15, 2023
0b0ddc6
Feature/import model by name (#218)
wanglam Jun 19, 2023
38250ab
Feat add basic model delete (#219)
wanglam Jun 19, 2023
724b195
feat: refresh model version data after deploy or undeploy complete (#…
wanglam Jun 20, 2023
13928d9
fix: fix error when search for model name when index hasn't created (…
ruanyl Jun 20, 2023
34c7157
build: add experimental release action (#221)
ruanyl Jun 20, 2023
54f8603
Merge 2.x to model registry (#289)
wanglam Nov 16, 2023
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
Prev Previous commit
Next Next commit
feat(ui): version information edit component (#180)
Signed-off-by: Yulong Ruan <ruanyl@amazon.com>
  • Loading branch information
ruanyl authored May 17, 2023
commit 1628954712fee1625ae4d5f70ed835c789d7c523
1 change: 1 addition & 0 deletions public/apis/model.ts
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ export interface ModelDetail extends ModelSearchItem {
content: string;
last_updated_time: number;
created_time: number;
model_format: string;
}

export interface ModelSearchResponse {
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { useForm } from 'react-hook-form';
import userEvent from '@testing-library/user-event';

import { render, screen } from '../../../../../test/test_utils';
import { ModelVersionNotesField } from '../model_version_notes_field';

const TestApp = ({ readOnly = false }: { readOnly?: boolean }) => {
const form = useForm({
defaultValues: { versionNotes: '' },
});

return (
<ModelVersionNotesField control={form.control} label="Version notes" readOnly={readOnly} />
);
};

describe('<ModelVersionNotesField />', () => {
it('should render a version notes textarea field', () => {
render(<TestApp />);
expect(screen.queryByRole('textbox')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toBeEnabled();
});

it('should render a readonly version notes input', () => {
render(<TestApp readOnly />);
expect(screen.getByRole('textbox')).toBeDisabled();
});

it('should only allow maximum 200 characters', async () => {
const user = userEvent.setup();
render(<TestApp />);
await user.type(screen.getByRole('textbox'), 'x'.repeat(201));
expect(screen.getByRole<HTMLTextAreaElement>('textbox').value).toHaveLength(200);
});
});
54 changes: 54 additions & 0 deletions public/components/common/forms/model_version_notes_field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { EuiFormRow, EuiTextArea } from '@elastic/eui';
import { FieldPathByValue, useController } from 'react-hook-form';
import type { Control } from 'react-hook-form';

interface VersionNotesFormData {
versionNotes?: string;
}

interface Props<T extends VersionNotesFormData> {
label: React.ReactNode;
control: Control<T>;
readOnly?: boolean;
}

const VERSION_NOTES_MAX_LENGTH = 200;

export const ModelVersionNotesField = <T extends VersionNotesFormData>({
control,
label,
readOnly = false,
}: Props<T>) => {
const fieldController = useController({
name: 'versionNotes' as FieldPathByValue<T, string>,
control,
});
const { ref, ...versionNotesField } = fieldController.field;

return (
<EuiFormRow
helpText={`${Math.max(
VERSION_NOTES_MAX_LENGTH - (versionNotesField.value?.length ?? 0),
0
)} characters ${versionNotesField.value?.length ? 'left' : 'allowed'}.`}
isInvalid={Boolean(fieldController.fieldState.error)}
error={fieldController.fieldState.error?.message}
label={label}
>
<EuiTextArea
disabled={readOnly}
inputRef={ref}
isInvalid={Boolean(fieldController.fieldState.error)}
maxLength={VERSION_NOTES_MAX_LENGTH}
style={{ height: 80 }}
{...versionNotesField}
/>
</EuiFormRow>
);
};
6 changes: 6 additions & 0 deletions public/components/model/types.ts
Original file line number Diff line number Diff line change
@@ -14,3 +14,9 @@ export interface VersionTableDataItem {
tags: { [key: string]: string | number };
createdTime: number;
}

export interface Tag {
key: string;
value: string;
type: 'number' | 'string';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import userEvent from '@testing-library/user-event';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { render, screen } from '../../../../test/test_utils';
import { ModelFileFormData, ModelUrlFormData } from '../types';
import { ModelVersionInformation } from '../version_information';

const TestApp = () => {
const form = useForm<ModelFileFormData | ModelUrlFormData>({
defaultValues: { versionNotes: 'test_version_notes' },
});

return (
<FormProvider {...form}>
<ModelVersionInformation />
</FormProvider>
);
};

describe('<ModelVersionInformation />', () => {
it('should display version notes as readonly by default', () => {
render(<TestApp />);
expect(screen.getByLabelText('edit version notes')).toBeEnabled();
expect(screen.getByDisplayValue('test_version_notes')).toBeDisabled();
});

it('should allow to edit version notes after clicking edit button', async () => {
const user = userEvent.setup();
render(<TestApp />);
expect(screen.getByDisplayValue('test_version_notes')).toBeDisabled();

await user.click(screen.getByLabelText('edit version notes'));
expect(screen.getByDisplayValue('test_version_notes')).toBeEnabled();
expect(screen.getByLabelText('cancel edit version notes')).toBeInTheDocument();
});

it('should reset the version notes changes and set the input to disabled after clicking cancel button', async () => {
const user = userEvent.setup();
render(<TestApp />);
await user.click(screen.getByLabelText('edit version notes'));
const versionNotesInput = screen.getByLabelText('Version notes');
expect(versionNotesInput).toBeEnabled();

await user.clear(versionNotesInput);
await user.type(versionNotesInput, 'new_test_version_notes');
// version notes input updated
expect(screen.getByDisplayValue('new_test_version_notes')).toBeInTheDocument();

await user.click(screen.getByLabelText('cancel edit version notes'));
// reset to default value after clicking cancel button
expect(screen.getByDisplayValue('test_version_notes')).toBeDisabled();
});
});
19 changes: 17 additions & 2 deletions public/components/model_version/model_version.tsx
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import {
} from '@elastic/eui';
import { generatePath, useHistory, useParams } from 'react-router-dom';

import { FormProvider, useForm } from 'react-hook-form';
import { useFetcher } from '../../hooks';
import { APIProvider } from '../../apis/api_provider';
import { routerPaths } from '../../../common/router_paths';
@@ -27,6 +28,7 @@ import { ModelVersionDetails } from './version_details';
import { ModelVersionInformation } from './version_information';
import { ModelVersionArtifact } from './version_artifact';
import { ModelVersionTags } from './version_tags';
import { ModelFileFormData, ModelUrlFormData } from './types';

export const ModelVersion = () => {
const { id: modelId } = useParams<{ id: string }>();
@@ -35,6 +37,7 @@ export const ModelVersion = () => {
const history = useHistory();
const modelName = model?.name;
const modelVersion = model?.model_version;
const form = useForm<ModelFileFormData | ModelUrlFormData>();

const onVersionChange = useCallback(
({ newVersion, newId }: { newVersion: string; newId: string }) => {
@@ -61,6 +64,18 @@ export const ModelVersion = () => {
});
}, [modelName, modelVersion]);

useEffect(() => {
if (model) {
form.reset({
versionNotes: 'TODO', // TODO: read from model.versionNotes
tags: [], // TODO: read from model.tags
configuration: JSON.stringify(model.model_config),
modelFileFormat: model.model_format,
// TODO: read model url or model filename
});
}
}, [model, form]);

const tabs = [
{
id: 'version-information',
@@ -101,7 +116,7 @@ export const ModelVersion = () => {
];

return (
<>
<FormProvider {...form}>
{!modelInfo ? (
<>
<EuiLoadingSpinner data-test-subj="modelVersionLoadingSpinner" />
@@ -148,6 +163,6 @@ export const ModelVersion = () => {
)}
<EuiSpacer size="m" />
<EuiTabbedContent tabs={tabs} />
</>
</FormProvider>
);
};
21 changes: 21 additions & 0 deletions public/components/model_version/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import type { Tag } from '../model/types';

interface FormDataBase {
versionNotes?: string;
tags?: Tag[];
configuration: string;
modelFileFormat: string;
}

export interface ModelFileFormData extends FormDataBase {
modelFile: File;
}

export interface ModelUrlFormData extends FormDataBase {
modelURL: string;
}
51 changes: 49 additions & 2 deletions public/components/model_version/version_information.tsx
Original file line number Diff line number Diff line change
@@ -11,10 +11,27 @@ import {
EuiSpacer,
EuiTitle,
EuiButton,
EuiText,
} from '@elastic/eui';
import React from 'react';
import React, { useState, useCallback } from 'react';
import { useFormContext, useFormState } from 'react-hook-form';
import { ModelVersionNotesField } from '../common/forms/model_version_notes_field';
import { ModelFileFormData, ModelUrlFormData } from './types';

export const ModelVersionInformation = () => {
const form = useFormContext<ModelFileFormData | ModelUrlFormData>();
const formState = useFormState({ control: form.control });
const [readOnly, setReadOnly] = useState(true);

const onCancel = useCallback(() => {
form.resetField('versionNotes');
setReadOnly(true);
}, [form]);

// Whether edit button is disabled or not
// The edit button should be disabled if there were changes in other form fields
const isEditDisabled = formState.isDirty && !formState.dirtyFields.versionNotes;

return (
<EuiPanel style={{ padding: 20 }}>
<EuiFlexGroup alignItems="center">
@@ -24,11 +41,41 @@ export const ModelVersionInformation = () => {
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem style={{ marginLeft: 'auto', flexGrow: 0 }}>
<EuiButton>Edit</EuiButton>
{readOnly ? (
<EuiButton
aria-label="edit version notes"
disabled={isEditDisabled}
onClick={() => setReadOnly(false)}
>
Edit
</EuiButton>
) : (
<EuiButton aria-label="cancel edit version notes" onClick={onCancel}>
Cancel
</EuiButton>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiHorizontalRule margin="none" />
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem style={{ maxWidth: 372 }}>
<EuiText>
<h4>
Version notes - <i style={{ fontWeight: 'normal' }}>optional</i>
</h4>
<small>{"Describe what's new about this version."}</small>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<ModelVersionNotesField
readOnly={readOnly}
label="Version notes"
control={form.control}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};
42 changes: 12 additions & 30 deletions public/components/register_model/model_version_notes.tsx
Original file line number Diff line number Diff line change
@@ -4,47 +4,29 @@
*/

import React from 'react';
import { EuiText, EuiSpacer, EuiFormRow, EuiTextArea } from '@elastic/eui';
import { useFormContext, useController } from 'react-hook-form';
import { EuiText, EuiSpacer } from '@elastic/eui';
import { useFormContext } from 'react-hook-form';

import type { ModelFileFormData, ModelUrlFormData } from './register_model.types';

const VERSION_NOTES_MAX_LENGTH = 200;
import { ModelVersionNotesField } from '../common/forms/model_version_notes_field';

export const ModelVersionNotesPanel = () => {
const { control } = useFormContext<ModelFileFormData | ModelUrlFormData>();

const fieldController = useController({
name: 'versionNotes',
control,
});
const { ref, ...versionNotesField } = fieldController.field;

return (
<div>
<EuiText size="s">
<h3>
Version notes - <i style={{ fontWeight: 300 }}>optional</i>
</h3>
<h3>Version information</h3>
</EuiText>
<EuiSpacer size="m" />
<EuiFormRow
helpText={`${Math.max(
VERSION_NOTES_MAX_LENGTH - (versionNotesField.value?.length ?? 0),
0
)} characters ${versionNotesField.value?.length ? 'left' : 'allowed'}.`}
isInvalid={Boolean(fieldController.fieldState.error)}
error={fieldController.fieldState.error?.message}
label="Notes"
>
<EuiTextArea
inputRef={ref}
isInvalid={Boolean(fieldController.fieldState.error)}
maxLength={VERSION_NOTES_MAX_LENGTH}
style={{ height: 80 }}
{...versionNotesField}
/>
</EuiFormRow>
<ModelVersionNotesField
control={control}
label={
<>
Version notes - <i style={{ fontWeight: 300 }}>optional</i>{' '}
</>
}
/>
</div>
);
};
Loading