-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Model Registry Settings: Add model registries table with db config vi…
…ewer, delete action and stub for permission management modal Signed-off-by: Mike Turley <[email protected]> Signed-off-by: Mike Turley <[email protected]>
- Loading branch information
Showing
17 changed files
with
629 additions
and
88 deletions.
There are no files selected for viewing
6 changes: 3 additions & 3 deletions
6
...d/src/routes/api/modelRegistries/index.ts → ...modelRegistrySettings/registries/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
backend/src/routes/api/modelRegistrySettings/secrets/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { FastifyReply, FastifyRequest } from 'fastify'; | ||
import { V1Secret } from '@kubernetes/client-node'; | ||
import { secureAdminRoute } from '../../../../utils/route-security'; | ||
import { KubeFastifyInstance, RecursivePartial } from '../../../../types'; | ||
import { MODEL_REGISTRY_NAMESPACE } from '../../../../utils/constants'; | ||
|
||
export default async (fastify: KubeFastifyInstance): Promise<void> => { | ||
fastify.post( | ||
'/', | ||
secureAdminRoute(fastify)( | ||
async ( | ||
request: FastifyRequest<{ | ||
Querystring: { dryRun?: string }; | ||
Body: V1Secret; | ||
}>, | ||
reply: FastifyReply, | ||
) => { | ||
const { dryRun } = request.query; | ||
const secret = request.body; | ||
try { | ||
const response = await fastify.kube.coreV1Api.createNamespacedSecret( | ||
MODEL_REGISTRY_NAMESPACE, | ||
secret, | ||
undefined, | ||
dryRun, | ||
); | ||
return response.body; | ||
} catch (e) { | ||
fastify.log.error( | ||
`Secret ${secret.metadata.name || secret.metadata.generateName} could not be created, ${ | ||
e.response?.body?.message || e.message | ||
}`, | ||
); | ||
reply.send(e); | ||
} | ||
}, | ||
), | ||
); | ||
|
||
fastify.get( | ||
'/:name', | ||
secureAdminRoute(fastify)( | ||
async (request: FastifyRequest<{ Params: { name: string } }>, reply: FastifyReply) => { | ||
const { name } = request.params; | ||
try { | ||
const response = await fastify.kube.coreV1Api.readNamespacedSecret( | ||
name, | ||
MODEL_REGISTRY_NAMESPACE, | ||
); | ||
return response.body; | ||
} catch (e) { | ||
fastify.log.error( | ||
`Secret ${name} could not be read, ${e.response?.body?.message || e.message}`, | ||
); | ||
reply.send(e); | ||
} | ||
}, | ||
), | ||
); | ||
|
||
fastify.patch( | ||
'/:name', | ||
secureAdminRoute(fastify)( | ||
async ( | ||
request: FastifyRequest<{ | ||
Querystring: { dryRun?: string }; | ||
Params: { name: string }; | ||
Body: RecursivePartial<V1Secret>; | ||
}>, | ||
reply: FastifyReply, | ||
) => { | ||
const { dryRun } = request.query; | ||
const { name } = request.params; | ||
try { | ||
const response = await fastify.kube.coreV1Api.patchNamespacedSecret( | ||
name, | ||
MODEL_REGISTRY_NAMESPACE, | ||
request.body, | ||
undefined, | ||
dryRun, | ||
); | ||
return response.body; | ||
} catch (e) { | ||
fastify.log.error( | ||
`Secret ${name} could not be modified, ${e.response?.body?.message || e.message}`, | ||
); | ||
reply.send(e); | ||
} | ||
}, | ||
), | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
frontend/src/concepts/modelRegistrySettings/useModelRegistriesBackend.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React from 'react'; | ||
import useFetchState, { FetchState } from '~/utilities/useFetchState'; | ||
import { ModelRegistryKind } from '~/k8sTypes'; | ||
import { listModelRegistriesBackend } from '~/services/modelRegistrySettingsService'; | ||
|
||
const useModelRegistriesBackend = (): FetchState<ModelRegistryKind[]> => { | ||
const getModelRegistries = React.useCallback(() => listModelRegistriesBackend(), []); | ||
return useFetchState<ModelRegistryKind[]>(getModelRegistries, []); | ||
}; | ||
|
||
export default useModelRegistriesBackend; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { ModelRegistryKind } from '~/k8sTypes'; | ||
import { getModelRegistrySecretBackend } from '~/services/modelRegistrySettingsService'; | ||
|
||
export const getModelRegistryDatabasePassword = async ( | ||
modelRegistry: ModelRegistryKind, | ||
): Promise<string | null> => { | ||
const dbSpec = modelRegistry.spec.mysql || modelRegistry.spec.postgres; | ||
const secretName = dbSpec?.passwordSecret?.name; | ||
if (!secretName) { | ||
return null; | ||
} | ||
const secret = await getModelRegistrySecretBackend(secretName); | ||
const encodedPassword = secret.data?.['database-password']; | ||
return encodedPassword ? atob(encodedPassword) : null; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import React from 'react'; | ||
import { Modal, TextContent, Text, TextInput, Stack, StackItem } from '@patternfly/react-core'; | ||
import { ModelRegistryKind } from '~/k8sTypes'; | ||
import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; | ||
import { deleteModelRegistryBackend } from '~/services/modelRegistrySettingsService'; | ||
|
||
type DeleteModelRegistryModalProps = { | ||
modelRegistry: ModelRegistryKind; | ||
isOpen: boolean; | ||
onClose: () => void; | ||
refresh: () => void; | ||
}; | ||
|
||
const DeleteModelRegistryModal: React.FC<DeleteModelRegistryModalProps> = ({ | ||
modelRegistry: mr, | ||
isOpen, | ||
onClose, | ||
refresh, | ||
}) => { | ||
const defaultPermissionsGroup = 'TODO: group here'; // TODO should be implemented as part of https://issues.redhat.com/browse/RHOAIENG-6636 | ||
const [isSubmitting, setIsSubmitting] = React.useState(false); | ||
const [error, setError] = React.useState<Error>(); | ||
const [confirmInputValue, setConfirmInputValue] = React.useState(''); | ||
const isDisabled = confirmInputValue.trim() !== mr.metadata.name || isSubmitting; | ||
|
||
const onBeforeClose = () => { | ||
setConfirmInputValue(''); | ||
setIsSubmitting(false); | ||
setError(undefined); | ||
onClose(); | ||
}; | ||
|
||
const onConfirm = async () => { | ||
setIsSubmitting(true); | ||
setError(undefined); | ||
try { | ||
await deleteModelRegistryBackend(mr.metadata.name); | ||
refresh(); | ||
onBeforeClose(); | ||
} catch (e) { | ||
if (e instanceof Error) { | ||
setError(e); | ||
} | ||
setIsSubmitting(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<Modal | ||
titleIconVariant="warning" | ||
title="Delete model registry?" | ||
isOpen={isOpen} | ||
onClose={onClose} | ||
variant="medium" | ||
footer={ | ||
<DashboardModalFooter | ||
submitLabel="Delete model registry" | ||
submitButtonVariant="danger" | ||
onSubmit={onConfirm} | ||
onCancel={onBeforeClose} | ||
isSubmitLoading={isSubmitting} | ||
isSubmitDisabled={isDisabled} | ||
error={error} | ||
alertTitle="Error deleting model registry" | ||
/> | ||
} | ||
> | ||
<Stack hasGutter> | ||
<StackItem> | ||
<TextContent> | ||
<Text component="p"> | ||
Only the <strong>{mr.metadata.name}</strong> itself will be removed. You'll need | ||
to manually delete all data in the connected database. Additionally, the default group{' '} | ||
<strong>{defaultPermissionsGroup}</strong> and any permissions associated with{' '} | ||
<strong>{mr.metadata.name}</strong> will be deleted. Any other groups and roles | ||
created by you will need to be manually deleted. | ||
</Text> | ||
<Text component="p"> | ||
Type <strong>{mr.metadata.name}</strong> to confirm deletion. | ||
</Text> | ||
</TextContent> | ||
</StackItem> | ||
<StackItem> | ||
<TextInput | ||
id="confirm-delete-input" | ||
data-testid="confirm-delete-input" | ||
aria-label="Confirm delete input" | ||
value={confirmInputValue} | ||
onChange={(_e, newValue) => setConfirmInputValue(newValue)} | ||
onKeyDown={(event) => { | ||
if (event.key === 'Enter' && !isDisabled) { | ||
onConfirm(); | ||
} | ||
}} | ||
/> | ||
</StackItem> | ||
</Stack> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default DeleteModelRegistryModal; |
43 changes: 43 additions & 0 deletions
43
frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import React from 'react'; | ||
import { Modal } from '@patternfly/react-core'; | ||
import { ModelRegistryKind } from '~/k8sTypes'; | ||
import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; | ||
|
||
type ManagePermissionsModalProps = { | ||
modelRegistry: ModelRegistryKind; | ||
isOpen: boolean; | ||
onClose: () => void; | ||
refresh: () => void; | ||
}; | ||
|
||
const ManagePermissionsModal: React.FC<ManagePermissionsModalProps> = ({ | ||
modelRegistry: mr, | ||
isOpen, | ||
onClose, | ||
refresh, | ||
}) => ( | ||
<Modal | ||
title={`Manage permissions of ${mr.metadata.name}`} | ||
description="Manage users and projects that can access this model registry." | ||
isOpen={isOpen} | ||
onClose={onClose} | ||
variant="medium" | ||
footer={ | ||
<DashboardModalFooter | ||
submitLabel="Save" | ||
onSubmit={() => { | ||
// TODO submit changes, then... | ||
refresh(); | ||
}} | ||
onCancel={onClose} | ||
isSubmitDisabled // TODO | ||
error={undefined} // TODO | ||
alertTitle="Error saving permissions" | ||
/> | ||
} | ||
> | ||
TODO: This feature is not yet implemented | ||
</Modal> | ||
); | ||
|
||
export default ManagePermissionsModal; |
45 changes: 45 additions & 0 deletions
45
frontend/src/pages/modelRegistrySettings/ModelRegistriesTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import React from 'react'; | ||
import { Button, Toolbar, ToolbarContent, ToolbarItem } from '@patternfly/react-core'; | ||
import { Table } from '~/components/table'; | ||
import { ModelRegistryKind } from '~/k8sTypes'; | ||
import { modelRegistryColumns } from './columns'; | ||
import ModelRegistriesTableRow from './ModelRegistriesTableRow'; | ||
|
||
type ModelRegistriesTableProps = { | ||
modelRegistries: ModelRegistryKind[]; | ||
refresh: () => void; | ||
onCreateModelRegistryClick: () => void; | ||
}; | ||
|
||
const ModelRegistriesTable: React.FC<ModelRegistriesTableProps> = ({ | ||
modelRegistries, | ||
refresh, | ||
onCreateModelRegistryClick, | ||
}) => ( | ||
<Table | ||
data-testid="model-registries-table" | ||
data={modelRegistries} | ||
columns={modelRegistryColumns} | ||
toolbarContent={ | ||
<Toolbar> | ||
<ToolbarContent> | ||
<ToolbarItem> | ||
<Button | ||
data-testid="create-model-registry-button" | ||
variant="primary" | ||
onClick={onCreateModelRegistryClick} | ||
> | ||
Create model registry | ||
</Button> | ||
</ToolbarItem> | ||
</ToolbarContent> | ||
</Toolbar> | ||
} | ||
rowRenderer={(mr) => ( | ||
<ModelRegistriesTableRow key={mr.metadata.name} modelRegistry={mr} refresh={refresh} /> | ||
)} | ||
variant="compact" | ||
/> | ||
); | ||
|
||
export default ModelRegistriesTable; |
Oops, something went wrong.