Skip to content

Commit 640cabe

Browse files
authored
feat(frontend): Create Model Registry Modal (#852)
Signed-off-by: Jenny <[email protected]> remove unused vars add name and description fields Signed-off-by: Jenny <[email protected]> format ResourceNameField.tsx
1 parent 99928e9 commit 640cabe

10 files changed

+741
-3
lines changed

clients/ui/frontend/src/app/pages/settings/ModelRegistriesTable.tsx

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
11
import React from 'react';
2+
import { Button, Toolbar, ToolbarContent, ToolbarItem } from '@patternfly/react-core';
23
import { ModelRegistry } from '~/app/types';
34
import { Table } from '~/shared/components/table';
45
import { modelRegistryColumns } from './columns';
56
import ModelRegistriesTableRow from './ModelRegistriesTableRow';
67

78
type ModelRegistriesTableProps = {
89
modelRegistries: ModelRegistry[];
10+
onCreateModelRegistryClick: () => void;
911
};
1012

11-
const ModelRegistriesTable: React.FC<ModelRegistriesTableProps> = ({ modelRegistries }) => (
13+
const ModelRegistriesTable: React.FC<ModelRegistriesTableProps> = ({
14+
modelRegistries,
15+
onCreateModelRegistryClick,
16+
}) => (
1217
// TODO: [Midstream] Complete once we have permissions
1318
<Table
1419
data-testid="model-registries-table"
1520
data={modelRegistries}
1621
columns={modelRegistryColumns}
22+
toolbarContent={
23+
<Toolbar>
24+
<ToolbarContent>
25+
<ToolbarItem>
26+
<Button
27+
data-testid="create-model-registry-button"
28+
variant="primary"
29+
onClick={onCreateModelRegistryClick}
30+
>
31+
Create model registry
32+
</Button>
33+
</ToolbarItem>
34+
</ToolbarContent>
35+
</Toolbar>
36+
}
1737
rowRenderer={(mr) => (
1838
<ModelRegistriesTableRow
1939
key={mr.name}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import * as React from 'react';
2+
import {
3+
Button,
4+
Form,
5+
FormGroup,
6+
HelperText,
7+
HelperTextItem,
8+
TextInput,
9+
} from '@patternfly/react-core';
10+
import { Modal } from '@patternfly/react-core/deprecated';
11+
import { useNavigate } from 'react-router';
12+
import ModelRegistryCreateModalFooter from '~/app/pages/settings/ModelRegistryCreateModalFooter';
13+
import FormSection from '~/shared/components/pf-overrides/FormSection';
14+
15+
import ModelRegistryDatabasePassword from '~/app/pages/settings/ModelRegistryDatabasePassword';
16+
import K8sNameDescriptionField from '~/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField';
17+
18+
type CreateModalProps = {
19+
onClose: () => void;
20+
// refresh: () => Promise<unknown>;
21+
// modelRegistry: ModelRegistry;
22+
};
23+
24+
const CreateModal: React.FC<CreateModalProps> = ({
25+
onClose,
26+
// refresh,
27+
// modelRegistry,
28+
}) => {
29+
const [error, setError] = React.useState<Error>();
30+
31+
const [host, setHost] = React.useState('');
32+
const [port, setPort] = React.useState('');
33+
const [username, setUsername] = React.useState('');
34+
const [password, setPassword] = React.useState('');
35+
const [database, setDatabase] = React.useState('');
36+
// const [addSecureDB, setAddSecureDB] = React.useState(false);
37+
const [isHostTouched, setIsHostTouched] = React.useState(false);
38+
const [isPortTouched, setIsPortTouched] = React.useState(false);
39+
const [isUsernameTouched, setIsUsernameTouched] = React.useState(false);
40+
const [isPasswordTouched, setIsPasswordTouched] = React.useState(false);
41+
const [isDatabaseTouched, setIsDatabaseTouched] = React.useState(false);
42+
const [showPassword, setShowPassword] = React.useState(false);
43+
44+
const navigate = useNavigate();
45+
46+
const onBeforeClose = () => {
47+
// setIsSubmitting(false);
48+
setError(undefined);
49+
50+
setHost('');
51+
setPort('');
52+
setUsername('');
53+
setPassword('');
54+
setDatabase('');
55+
setIsHostTouched(false);
56+
setIsPortTouched(false);
57+
setIsUsernameTouched(false);
58+
setIsPasswordTouched(false);
59+
setIsDatabaseTouched(false);
60+
setShowPassword(false);
61+
onClose();
62+
};
63+
64+
const hasContent = (value: string): boolean => !!value.trim().length;
65+
66+
const canSubmit = () =>
67+
// TODO: implement once we have the endpoint
68+
// !isSubmitting &&
69+
// isValidK8sName(nameDesc.k8sName.value || translateDisplayNameForK8s(nameDesc.name))
70+
// &&
71+
hasContent(host) &&
72+
hasContent(password) &&
73+
hasContent(port) &&
74+
hasContent(username) &&
75+
hasContent(database);
76+
// &&
77+
// (!addSecureDB || (secureDBInfo.isValid && !configSecretsError))
78+
79+
const onSubmit = () => {
80+
navigate(`/model-registry-settings`);
81+
onClose();
82+
};
83+
84+
return (
85+
<Modal
86+
isOpen
87+
title="Create model registry"
88+
onClose={onBeforeClose}
89+
actions={[
90+
<Button key="create-button" variant="primary" isDisabled={!canSubmit()} onClick={onSubmit}>
91+
Create
92+
</Button>,
93+
<Button key="cancel-button" variant="secondary" onClick={onBeforeClose}>
94+
Cancel
95+
</Button>,
96+
]}
97+
variant="medium"
98+
footer={
99+
<ModelRegistryCreateModalFooter
100+
onCancel={onBeforeClose}
101+
onSubmit={onSubmit}
102+
submitLabel="Create"
103+
// isSubmitLoading={isSubmitting}
104+
isSubmitDisabled={!canSubmit()}
105+
error={error}
106+
alertTitle={`Error ${'creating'} model registry`}
107+
/>
108+
}
109+
>
110+
<Form>
111+
<K8sNameDescriptionField
112+
dataTestId="mr"
113+
// data={nameDesc}
114+
// onDataChange={setNameDesc}
115+
/>
116+
<FormSection
117+
title="Connect to external MySQL database"
118+
description="This external database is where model data is stored."
119+
>
120+
<FormGroup label="Host" isRequired fieldId="mr-host">
121+
<TextInput
122+
isRequired
123+
type="text"
124+
id="mr-host"
125+
name="mr-host"
126+
value={host}
127+
onBlur={() => setIsHostTouched(true)}
128+
onChange={(_e, value) => setHost(value)}
129+
validated={isHostTouched && !hasContent(host) ? 'error' : 'default'}
130+
/>
131+
{isHostTouched && !hasContent(host) && (
132+
<HelperText>
133+
<HelperTextItem variant="error" data-testid="mr-host-error">
134+
Host cannot be empty
135+
</HelperTextItem>
136+
</HelperText>
137+
)}
138+
</FormGroup>
139+
<FormGroup label="Port" isRequired fieldId="mr-port">
140+
<TextInput
141+
isRequired
142+
type="text"
143+
id="mr-port"
144+
name="mr-port"
145+
value={port}
146+
onBlur={() => setIsPortTouched(true)}
147+
onChange={(_e, value) => setPort(value)}
148+
validated={isPortTouched && !hasContent(port) ? 'error' : 'default'}
149+
/>
150+
{isPortTouched && !hasContent(port) && (
151+
<HelperText>
152+
<HelperTextItem variant="error" data-testid="mr-port-error">
153+
Port cannot be empty
154+
</HelperTextItem>
155+
</HelperText>
156+
)}
157+
</FormGroup>
158+
<FormGroup label="Username" isRequired fieldId="mr-username">
159+
<TextInput
160+
isRequired
161+
type="text"
162+
id="mr-username"
163+
name="mr-username"
164+
value={username}
165+
onBlur={() => setIsUsernameTouched(true)}
166+
onChange={(_e, value) => setUsername(value)}
167+
validated={isUsernameTouched && !hasContent(username) ? 'error' : 'default'}
168+
/>
169+
{isUsernameTouched && !hasContent(username) && (
170+
<HelperText>
171+
<HelperTextItem variant="error" data-testid="mr-username-error">
172+
Username cannot be empty
173+
</HelperTextItem>
174+
</HelperText>
175+
)}
176+
</FormGroup>
177+
<FormGroup label="Password" isRequired fieldId="mr-password">
178+
<ModelRegistryDatabasePassword
179+
password={password || ''}
180+
setPassword={setPassword}
181+
isPasswordTouched={isPasswordTouched}
182+
setIsPasswordTouched={setIsPasswordTouched}
183+
showPassword={showPassword}
184+
// editRegistry={mr}
185+
/>
186+
</FormGroup>
187+
<FormGroup label="Database" isRequired fieldId="mr-database">
188+
<TextInput
189+
isRequired
190+
type="text"
191+
id="mr-database"
192+
name="mr-database"
193+
value={database}
194+
onBlur={() => setIsDatabaseTouched(true)}
195+
onChange={(_e, value) => setDatabase(value)}
196+
validated={isDatabaseTouched && !hasContent(database) ? 'error' : 'default'}
197+
/>
198+
{isDatabaseTouched && !hasContent(database) && (
199+
<HelperText>
200+
<HelperTextItem variant="error" data-testid="mr-database-error">
201+
Database cannot be empty
202+
</HelperTextItem>
203+
</HelperText>
204+
)}
205+
</FormGroup>
206+
{/* {secureDbEnabled && (
207+
<>
208+
<FormGroup>
209+
<Checkbox
210+
label="Add CA certificate to secure database connection"
211+
isChecked={addSecureDB}
212+
onChange={(_e, value) => setAddSecureDB(value)}
213+
id="add-secure-db"
214+
data-testid="add-secure-db-mr-checkbox"
215+
name="add-secure-db"
216+
/>
217+
</FormGroup>
218+
{addSecureDB &&
219+
(!configSecretsLoaded && !configSecretsError ? (
220+
<EmptyState icon={Spinner} />
221+
) : configSecretsLoaded ? (
222+
<CreateMRSecureDBSection
223+
secureDBInfo={secureDBInfo}
224+
modelRegistryNamespace={modelRegistryNamespace}
225+
k8sName={nameDesc.k8sName.value}
226+
existingCertConfigMaps={configSecrets.configMaps}
227+
existingCertSecrets={configSecrets.secrets}
228+
setSecureDBInfo={setSecureDBInfo}
229+
/>
230+
) : (
231+
<Alert
232+
isInline
233+
variant="danger"
234+
title="Error fetching config maps and secrets"
235+
data-testid="error-fetching-resource-alert"
236+
>
237+
{configSecretsError?.message}
238+
</Alert>
239+
))}
240+
</>
241+
)} */}
242+
</FormSection>
243+
</Form>
244+
</Modal>
245+
);
246+
};
247+
248+
export default CreateModal;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as React from 'react';
2+
import {
3+
ActionList,
4+
ActionListItem,
5+
ActionListGroup,
6+
Alert,
7+
Button,
8+
ButtonProps,
9+
Stack,
10+
StackItem,
11+
} from '@patternfly/react-core';
12+
13+
type ModelRegistryCreateModalFooterProps = {
14+
submitLabel: string;
15+
submitButtonVariant?: ButtonProps['variant'];
16+
onSubmit: () => void;
17+
onCancel: () => void;
18+
isSubmitDisabled?: boolean;
19+
isSubmitLoading?: boolean;
20+
isCancelDisabled?: boolean;
21+
alertTitle?: string;
22+
error?: Error;
23+
alertLinks?: React.ReactNode;
24+
};
25+
26+
const ModelRegistryCreateModalFooter: React.FC<ModelRegistryCreateModalFooterProps> = ({
27+
submitLabel,
28+
submitButtonVariant = 'primary',
29+
onSubmit,
30+
onCancel,
31+
isSubmitDisabled,
32+
isSubmitLoading,
33+
isCancelDisabled,
34+
error,
35+
alertTitle,
36+
alertLinks,
37+
}) => (
38+
// make sure alert uses the full width
39+
<Stack hasGutter style={{ flex: 'auto' }}>
40+
{error && (
41+
<StackItem>
42+
<Alert
43+
data-testid="error-message-alert"
44+
isInline
45+
variant="danger"
46+
title={alertTitle}
47+
actionLinks={alertLinks}
48+
>
49+
{error.message}
50+
</Alert>
51+
</StackItem>
52+
)}
53+
<StackItem>
54+
<ActionList>
55+
<ActionListGroup>
56+
<ActionListItem>
57+
<Button
58+
key="submit"
59+
variant={submitButtonVariant}
60+
isDisabled={isSubmitDisabled}
61+
onClick={onSubmit}
62+
isLoading={isSubmitLoading}
63+
data-testid="modal-submit-button"
64+
>
65+
{submitLabel}
66+
</Button>
67+
</ActionListItem>
68+
<ActionListItem>
69+
<Button
70+
key="cancel"
71+
variant="link"
72+
isDisabled={isCancelDisabled}
73+
onClick={onCancel}
74+
data-testid="modal-cancel-button"
75+
>
76+
Cancel
77+
</Button>
78+
</ActionListItem>
79+
</ActionListGroup>
80+
</ActionList>
81+
</StackItem>
82+
</Stack>
83+
);
84+
85+
export default ModelRegistryCreateModalFooter;

0 commit comments

Comments
 (0)