Skip to content

Commit

Permalink
Model Registry Settings: Add model registries table with db config vi…
Browse files Browse the repository at this point in the history
…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
mturley committed May 21, 2024
1 parent cd93b29 commit 30a5b04
Show file tree
Hide file tree
Showing 17 changed files with 629 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { PatchUtils } from '@kubernetes/client-node';
import { secureAdminRoute } from '../../../utils/route-security';
import { KubeFastifyInstance, ModelRegistryKind, RecursivePartial } from '../../../types';
import { MODEL_REGISTRY_NAMESPACE } from '../../../utils/constants';
import { secureAdminRoute } from '../../../../utils/route-security';
import { KubeFastifyInstance, ModelRegistryKind, RecursivePartial } from '../../../../types';
import { MODEL_REGISTRY_NAMESPACE } from '../../../../utils/constants';

export default async (fastify: KubeFastifyInstance): Promise<void> => {
fastify.get(
Expand Down
92 changes: 92 additions & 0 deletions backend/src/routes/api/modelRegistrySettings/secrets/index.ts
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);
}
},
),
);
};
8 changes: 7 additions & 1 deletion backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1045,9 +1045,15 @@ export type ModelRegistryKind = K8sResourceCommon & {
mysql?: {
database: string;
host: string;
passwordSecret?: {
key: string;
name: string;
};
port?: number;
skipDBCreation?: boolean;
username?: string;
};
postgres: {
postgres?: {
database: string;
host?: string;
passwordSecret?: {
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/concepts/dashboard/DashboardModalFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import {
ActionListItem,
Alert,
Button,
ButtonProps,
Stack,
StackItem,
} from '@patternfly/react-core';

type DashboardModalFooterProps = {
submitLabel: string;
submitButtonVariant?: ButtonProps['variant'];
onSubmit: () => void;
onCancel: () => void;
isSubmitDisabled: boolean;
Expand All @@ -21,6 +23,7 @@ type DashboardModalFooterProps = {

const DashboardModalFooter: React.FC<DashboardModalFooterProps> = ({
submitLabel,
submitButtonVariant = 'primary',
onSubmit,
onCancel,
isSubmitDisabled,
Expand All @@ -43,7 +46,7 @@ const DashboardModalFooter: React.FC<DashboardModalFooterProps> = ({
<ActionListItem>
<Button
key="submit"
variant="primary"
variant={submitButtonVariant}
isDisabled={isSubmitDisabled}
onClick={onSubmit}
isLoading={isSubmitLoading}
Expand Down
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, []);

Check warning on line 8 in frontend/src/concepts/modelRegistrySettings/useModelRegistriesBackend.ts

View check run for this annotation

Codecov / codecov/patch

frontend/src/concepts/modelRegistrySettings/useModelRegistriesBackend.ts#L6-L8

Added lines #L6 - L8 were not covered by tests
};

export default useModelRegistriesBackend;
15 changes: 15 additions & 0 deletions frontend/src/concepts/modelRegistrySettings/utils.ts
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 (

Check warning on line 4 in frontend/src/concepts/modelRegistrySettings/utils.ts

View check run for this annotation

Codecov / codecov/patch

frontend/src/concepts/modelRegistrySettings/utils.ts#L4

Added line #L4 was not covered by tests
modelRegistry: ModelRegistryKind,
): Promise<string | null> => {
const dbSpec = modelRegistry.spec.mysql || modelRegistry.spec.postgres;
const secretName = dbSpec?.passwordSecret?.name;
if (!secretName) {
return null;

Check warning on line 10 in frontend/src/concepts/modelRegistrySettings/utils.ts

View check run for this annotation

Codecov / codecov/patch

frontend/src/concepts/modelRegistrySettings/utils.ts#L6-L10

Added lines #L6 - L10 were not covered by tests
}
const secret = await getModelRegistrySecretBackend(secretName);
const encodedPassword = secret.data?.['database-password'];
return encodedPassword ? atob(encodedPassword) : null;

Check warning on line 14 in frontend/src/concepts/modelRegistrySettings/utils.ts

View check run for this annotation

Codecov / codecov/patch

frontend/src/concepts/modelRegistrySettings/utils.ts#L12-L14

Added lines #L12 - L14 were not covered by tests
};
8 changes: 7 additions & 1 deletion frontend/src/k8sTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1303,9 +1303,15 @@ export type ModelRegistryKind = K8sResourceCommon & {
mysql?: {
database: string;
host: string;
passwordSecret?: {
key: string;
name: string;
};
port?: number;
skipDBCreation?: boolean;
username?: string;
};
postgres: {
postgres?: {
database: string;
host?: string;
passwordSecret?: {
Expand Down
102 changes: 102 additions & 0 deletions frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx
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> = ({

Check warning on line 14 in frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx#L14

Added line #L14 was not covered by tests
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;

Check warning on line 24 in frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx#L19-L24

Added lines #L19 - L24 were not covered by tests

const onBeforeClose = () => {
setConfirmInputValue('');
setIsSubmitting(false);
setError(undefined);
onClose();

Check warning on line 30 in frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx#L26-L30

Added lines #L26 - L30 were not covered by tests
};

const onConfirm = async () => {
setIsSubmitting(true);
setError(undefined);
try {
await deleteModelRegistryBackend(mr.metadata.name);
refresh();
onBeforeClose();

Check warning on line 39 in frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx#L33-L39

Added lines #L33 - L39 were not covered by tests
} catch (e) {
if (e instanceof Error) {
setError(e);

Check warning on line 42 in frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx#L41-L42

Added lines #L41 - L42 were not covered by tests
}
setIsSubmitting(false);

Check warning on line 44 in frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx#L44

Added line #L44 was not covered by tests
}
};

return (

Check warning on line 48 in frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx#L48

Added line #L48 was not covered by tests
<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&apos;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();

Check warning on line 92 in frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/DeleteModelRegistryModal.tsx#L89-L92

Added lines #L89 - L92 were not covered by tests
}
}}
/>
</StackItem>
</Stack>
</Modal>
);
};

export default DeleteModelRegistryModal;
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> = ({

Check warning on line 13 in frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx#L13

Added line #L13 was not covered by tests
modelRegistry: mr,
isOpen,
onClose,
refresh,
}) => (
<Modal

Check warning on line 19 in frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx#L19

Added line #L19 was not covered by tests
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={() => {

Check warning on line 28 in frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx#L28

Added line #L28 was not covered by tests
// TODO submit changes, then...
refresh();

Check warning on line 30 in frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/ManagePermissionsModal.tsx#L30

Added line #L30 was not covered by tests
}}
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 frontend/src/pages/modelRegistrySettings/ModelRegistriesTable.tsx
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> = ({

Check warning on line 14 in frontend/src/pages/modelRegistrySettings/ModelRegistriesTable.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/ModelRegistriesTable.tsx#L14

Added line #L14 was not covered by tests
modelRegistries,
refresh,
onCreateModelRegistryClick,
}) => (
<Table

Check warning on line 19 in frontend/src/pages/modelRegistrySettings/ModelRegistriesTable.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/ModelRegistriesTable.tsx#L19

Added line #L19 was not covered by tests
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} />

Check warning on line 39 in frontend/src/pages/modelRegistrySettings/ModelRegistriesTable.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/modelRegistrySettings/ModelRegistriesTable.tsx#L39

Added line #L39 was not covered by tests
)}
variant="compact"
/>
);

export default ModelRegistriesTable;
Loading

0 comments on commit 30a5b04

Please sign in to comment.