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

added optional advanced section on entry creation for adding users to… #1323

Merged
4 changes: 2 additions & 2 deletions backend/src/clients/inferencing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function createInferenceService(inferenceServiceParams: InferenceSe
try {
res = await fetch(`${config.ui.inference.connection.host}/api/deploy`, {
method: 'POST',
headers: new Headers({ 'content-type': 'application/json' }),
headers: { 'content-type': 'application/json' },
body: JSON.stringify(inferenceServiceParams),
})
} catch (err) {
Expand All @@ -34,7 +34,7 @@ export async function updateInferenceService(inferenceServiceParams: InferenceSe
try {
res = await fetch(`${config.ui.inference.connection.host}/api/update`, {
method: 'PATCH',
headers: new Headers({ 'content-type': 'application/json' }),
headers: { 'content-type': 'application/json' },
body: JSON.stringify(inferenceServiceParams),
})
} catch (err) {
Expand Down
9 changes: 9 additions & 0 deletions backend/src/routes/v2/model/postModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ export const postModelSchema = z.object({
teamId: z.string(),
description: z.string(),
visibility: z.nativeEnum(EntryVisibility).optional().default(EntryVisibility.Public),
collaborators: z
.array(
z.object({
entity: z.string().openapi({ example: 'user:user' }),
roles: z.array(z.string()).openapi({ example: ['owner', 'contributor'] }),
}),
)
.optional()
.default([]),
settings: z
.object({
ungovernedAccess: z.boolean().optional().default(false).openapi({ example: true }),
Expand Down
31 changes: 24 additions & 7 deletions backend/src/services/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import _ from 'lodash'
import authentication from '../connectors/authentication/index.js'
import { ModelAction, ModelActionKeys } from '../connectors/authorisation/actions.js'
import authorisation from '../connectors/authorisation/index.js'
import ModelModel, { EntryKindKeys } from '../models/Model.js'
import ModelModel, { CollaboratorEntry, EntryKindKeys } from '../models/Model.js'
import Model, { ModelInterface } from '../models/Model.js'
import ModelCardRevisionModel, { ModelCardRevisionDoc } from '../models/ModelCardRevision.js'
import { UserInterface } from '../models/User.js'
Expand All @@ -21,21 +21,38 @@ export function checkModelRestriction(model: ModelInterface) {
}
}

export type CreateModelParams = Pick<ModelInterface, 'name' | 'teamId' | 'description' | 'visibility' | 'settings'>
export type CreateModelParams = Pick<
ModelInterface,
'name' | 'teamId' | 'description' | 'visibility' | 'settings' | 'kind' | 'collaborators'
>
export async function createModel(user: UserInterface, modelParams: CreateModelParams) {
const modelId = convertStringToId(modelParams.name)

// TODO - Find team by teamId to check it's valid. Throw error if not found.

const model = new Model({
...modelParams,
id: modelId,
collaborators: [
let collaborators: CollaboratorEntry[] = []
if (modelParams.collaborators && modelParams.collaborators.length > 0) {
const collaboratorListContainsOwner = modelParams.collaborators.some((collaborator) =>
collaborator.roles.some((role) => role === 'owner'),
)
if (collaboratorListContainsOwner) {
collaborators = modelParams.collaborators
} else {
throw BadReq('At least one collaborator must be given the owner role.')
}
} else {
collaborators = [
{
entity: toEntity('user', user.dn),
roles: ['owner'],
},
],
]
}

const model = new Model({
...modelParams,
id: modelId,
collaborators,
})

const auth = await authorisation.model(user, model, ModelAction.Create)
Expand Down
28 changes: 4 additions & 24 deletions backend/test/clients/__snapshots__/inferencing.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,8 @@ exports[`clients > inferencing > createInferencing > success 1`] = `
"http://example.com/api/deploy",
{
"body": "{}",
"headers": Headers {
Symbol(headers list): HeadersList {
"cookies": null,
Symbol(headers map): Map {
"content-type" => {
"name": "content-type",
"value": "application/json",
},
},
Symbol(headers map sorted): null,
},
Symbol(guard): "none",
"headers": {
"content-type": "application/json",
},
"method": "POST",
},
Expand All @@ -31,18 +21,8 @@ exports[`clients > inferencing > updateInferencing > success 1`] = `
"http://example.com/api/update",
{
"body": "{}",
"headers": Headers {
Symbol(headers list): HeadersList {
"cookies": null,
Symbol(headers map): Map {
"content-type" => {
"name": "content-type",
"value": "application/json",
},
},
Symbol(headers map sorted): null,
},
Symbol(guard): "none",
"headers": {
"content-type": "application/json",
},
"method": "PATCH",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,3 @@ exports[`routes > release > patchReleaseComment > audit > expected call 1`] = `
"message": "test",
}
`;

exports[`routes > release > postReleaseComment > 200 > ok 1`] = `
{
"release": {
"message": "test",
},
}
`;

exports[`routes > release > postReleaseComment > audit > expected call 1`] = `
{
"message": "test",
}
`;
204 changes: 134 additions & 70 deletions frontend/src/entry/CreateEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ArrowBack, FileUpload, Lock, LockOpen } from '@mui/icons-material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import LoadingButton from '@mui/lab/LoadingButton'
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Card,
Expand All @@ -13,13 +17,25 @@ import {
Typography,
} from '@mui/material'
import { postModel } from 'actions/model'
import { useGetCurrentUser } from 'actions/user'
import { useRouter } from 'next/router'
import { FormEvent, useMemo, useState } from 'react'
import Loading from 'src/common/Loading'
import EntryDescriptionInput from 'src/entry/EntryDescriptionInput'
import EntryNameInput from 'src/entry/EntryNameInput'
import EntryAccess from 'src/entry/settings/EntryAccess'
import MessageAlert from 'src/MessageAlert'
import TeamSelect from 'src/TeamSelect'
import { EntryForm, EntryKind, EntryKindKeys, EntryKindLabel, EntryVisibility, TeamInterface } from 'types/types'
import {
CollaboratorEntry,
EntityKind,
EntryForm,
EntryKind,
EntryKindKeys,
EntryKindLabel,
EntryVisibility,
TeamInterface,
} from 'types/types'
import { getErrorMessage } from 'utils/fetcher'
import { toTitleCase } from 'utils/stringUtils'

Expand All @@ -30,10 +46,16 @@ type CreateEntryProps = {

export default function CreateEntry({ kind, onBackClick }: CreateEntryProps) {
const router = useRouter()

const { currentUser, isCurrentUserLoading, isCurrentUserError } = useGetCurrentUser()

const [team, setTeam] = useState<TeamInterface | undefined>()
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [visibility, setVisibility] = useState<EntryForm['visibility']>(EntryVisibility.Public)
const [collaborators, setCollaborators] = useState<CollaboratorEntry[]>(
currentUser ? [{ entity: `${EntityKind.USER}:${currentUser?.dn}`, roles: ['owner'] }] : [],
)
const [errorMessage, setErrorMessage] = useState('')
const [loading, setLoading] = useState(false)

Expand All @@ -50,6 +72,7 @@ export default function CreateEntry({ kind, onBackClick }: CreateEntryProps) {
kind,
description,
visibility,
collaborators,
}
const response = await postModel(formData)

Expand Down Expand Up @@ -90,77 +113,118 @@ export default function CreateEntry({ kind, onBackClick }: CreateEntryProps) {
)
}

if (isCurrentUserError) {
return <MessageAlert message={isCurrentUserError.info.message} severity='error' />
}

return (
<Card sx={{ p: 4, mb: 4 }}>
<Stack spacing={1}>
<Button sx={{ width: 'fit-content' }} startIcon={<ArrowBack />} onClick={() => onBackClick()}>
Back
</Button>
<Stack spacing={2} alignItems='center' justifyContent='center'>
<Typography variant='h6' component='h1' color='primary'>
{`Create ${toTitleCase(kind)}`}
</Typography>
<FileUpload color='primary' fontSize='large' />
{kind === EntryKind.MODEL && (
<Typography>A model repository contains all files, history and information related to a model.</Typography>
)}
</Stack>
</Stack>
<Box component='form' sx={{ mt: 4 }} onSubmit={handleSubmit}>
<Stack divider={<Divider orientation='vertical' flexItem />} spacing={2}>
<>
<Typography component='h2' variant='h6'>
Overview
</Typography>
<Stack spacing={2} direction={{ xs: 'column', sm: 'row' }}>
<TeamSelect value={team} onChange={(value) => setTeam(value)} />
<EntryNameInput autoFocus value={name} kind={kind} onChange={(value) => setName(value)} />
</Stack>
<EntryDescriptionInput value={description} onChange={(value) => setDescription(value)} />
</>
<Divider />
<>
<Typography component='h3' variant='h6'>
Access control
<>
{isCurrentUserLoading && <Loading />}
<Card sx={{ p: 4, mb: 4 }}>
<Stack spacing={1}>
<Button sx={{ width: 'fit-content' }} startIcon={<ArrowBack />} onClick={() => onBackClick()}>
Back
</Button>
<Stack spacing={2} alignItems='center' justifyContent='center'>
<Typography variant='h6' component='h1' color='primary'>
{`Create ${toTitleCase(kind)}`}
</Typography>
<RadioGroup
defaultValue='public'
value={visibility}
onChange={(e) => setVisibility(e.target.value as EntryForm['visibility'])}
>
<FormControlLabel
value='public'
control={<Radio />}
label={publicLabel()}
data-test='publicButtonSelector'
/>
<FormControlLabel
value='private'
control={<Radio />}
label={privateLabel()}
data-test='privateButtonSelector'
/>
</RadioGroup>
</>
<Divider />
<Box sx={{ textAlign: 'right' }}>
<Tooltip title={!isFormValid ? 'Please make sure all required fields are filled out' : ''}>
<span>
<LoadingButton
variant='contained'
disabled={!isFormValid}
type='submit'
data-test='createEntryButton'
loading={loading}
>
{`Create ${EntryKindLabel[kind]}`}
</LoadingButton>
</span>
</Tooltip>
<MessageAlert message={errorMessage} severity='error' />
</Box>
<FileUpload color='primary' fontSize='large' />
{kind === EntryKind.MODEL && (
<Typography>
A model repository contains all files, history and information related to a model.
</Typography>
)}
</Stack>
</Stack>
</Box>
</Card>
<Box component='form' sx={{ mt: 4 }} onSubmit={handleSubmit}>
<Stack divider={<Divider orientation='vertical' flexItem />} spacing={2}>
<>
<Typography component='h2' variant='h6'>
Overview
</Typography>
<Stack spacing={2} direction={{ xs: 'column', sm: 'row' }}>
<TeamSelect value={team} onChange={(value) => setTeam(value)} />
<EntryNameInput autoFocus value={name} kind={kind} onChange={(value) => setName(value)} />
</Stack>
<EntryDescriptionInput value={description} onChange={(value) => setDescription(value)} />
</>
<Divider />
<>
<Typography component='h3' variant='h6'>
Access control
</Typography>
<RadioGroup
defaultValue='public'
value={visibility}
onChange={(e) => setVisibility(e.target.value as EntryForm['visibility'])}
>
<FormControlLabel
value='public'
control={<Radio />}
label={publicLabel()}
data-test='publicButtonSelector'
/>
<FormControlLabel
value='private'
control={<Radio />}
label={privateLabel()}
data-test='privateButtonSelector'
/>
</RadioGroup>
</>
<Accordion sx={{ borderTop: 'none' }}>
<AccordionSummary
sx={{ pl: 0 }}
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1-content'
id='panel1-header'
>
<Typography component='h3' variant='h6'>
Advanced (optional)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography variant='h6' component='h2'>
Manage access list
</Typography>
<Typography variant='caption'>
Please note that only entry roles can be added at this stage, and review roles should be added once a
schema has been selected.
</Typography>
<Box sx={{ marginTop: 1 }}>
<EntryAccess
value={collaborators}
onUpdate={(val) => setCollaborators(val)}
entryKind={EntryKind.MODEL}
entryRoles={[
{ id: 'owner', name: 'Owner' },
{ id: 'contributor', name: 'Contributor' },
{ id: 'consumer', name: 'Consumer' },
]}
/>
</Box>
</AccordionDetails>
</Accordion>
<Box sx={{ textAlign: 'right' }}>
<Tooltip title={!isFormValid ? 'Please make sure all required fields are filled out' : ''}>
<span>
<LoadingButton
variant='contained'
disabled={!isFormValid}
type='submit'
data-test='createEntryButton'
loading={loading}
>
{`Create ${EntryKindLabel[kind]}`}
</LoadingButton>
</span>
</Tooltip>
<MessageAlert message={errorMessage} severity='error' />
</Box>
</Stack>
</Box>
</Card>
</>
)
}
Loading
Loading