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 new optional configurable organisation and state fields for ent… #1892

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions backend/config/default.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ module.exports = {
helpPopoverText: {
manualEntryAccess: '',
},

modelDetails: {
organisations: ['Example Organisation'],
states: ['Development', 'Review', 'Production'],
},
},

connectors: {
Expand Down
7,485 changes: 2,093 additions & 5,392 deletions backend/package-lock.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions backend/src/migrations/014_add_organisation_and_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ModelModel from '../models/Model.js'

export async function up() {
await ModelModel.updateMany({ organisation: { $exists: false } }, { $set: { organisation: '' } })
await ModelModel.updateMany({ state: { $exists: false } }, { $set: { state: '' } })
}

export async function down() {
/* NOOP */
}
17 changes: 16 additions & 1 deletion backend/src/models/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export interface ModelInterface {
id: string

name: string
organisation: string
state: string
kind: EntryKindKeys
description: string
card?: ModelCardInterface
Expand All @@ -73,8 +75,21 @@ export type ModelDoc = ModelInterface & Document<any, any, ModelInterface>
const ModelSchema = new Schema<ModelInterface>(
{
id: { type: String, required: true, unique: true, index: true },

name: { type: String, required: true },
organisation: {
type: String,
required: function () {
return typeof this['organisation'] === 'string' ? false : true
},
default: '',
},
state: {
type: String,
required: function () {
return typeof this['state'] === 'string' ? false : true
},
default: '',
},
kind: { type: String, enum: Object.values(EntryKind) },
description: { type: String, required: true },
card: {
Expand Down
7 changes: 7 additions & 0 deletions backend/src/routes/v2/model/patchModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ import audit from '../../../connectors/audit/index.js'
import { EntryKind, EntryVisibility, ModelInterface } from '../../../models/Model.js'
import { updateModel } from '../../../services/model.js'
import { modelInterfaceSchema, registerPath } from '../../../services/specification.js'
import config from '../../../utils/config.js'
import { parse } from '../../../utils/validate.js'

const organisationsList = [...config.ui.modelDetails.organisations, '']
const statesList = [...config.ui.modelDetails.states, '']

export const patchModelSchema = z.object({
body: z.object({
name: z.string().optional().openapi({ example: 'Yolo v4' }),

kind: z.nativeEnum(EntryKind).optional().openapi({ example: EntryKind.Model }),
organisation: z.enum(organisationsList as [string, ...string[]]).optional(),
state: z.enum(statesList as [string, ...string[]]).optional(),
description: z.string().optional().openapi({ example: 'You only look once' }),
visibility: z.nativeEnum(EntryVisibility).optional().openapi({ example: 'private' }),
settings: z
Expand Down
5 changes: 5 additions & 0 deletions backend/src/routes/v2/model/postModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import audit from '../../../connectors/audit/index.js'
import { EntryKind, EntryVisibility, ModelInterface } from '../../../models/Model.js'
import { createModel } from '../../../services/model.js'
import { modelInterfaceSchema, registerPath } from '../../../services/specification.js'
import config from '../../../utils/config.js'
import { parse } from '../../../utils/validate.js'

const organisationsList = [...config.ui.modelDetails.organisations, '']

export const postModelSchema = z.object({
body: z.object({
name: z.string().min(1, 'You must provide a model name').openapi({ example: 'Yolo v4' }),
organisation: z.enum(organisationsList as [string, ...string[]]).optional(),
state: z.enum(config.ui.modelDetails.states as [string, ...string[], '']).optional(),
kind: z.nativeEnum(EntryKind).openapi({ example: 'model' }),
description: z.string().min(1, 'You must provide a model description').openapi({ example: 'You only look once' }),
visibility: z.nativeEnum(EntryVisibility).optional().default(EntryVisibility.Public),
Expand Down
2 changes: 2 additions & 0 deletions backend/src/seeds/data/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const model: ModelInterface = {
name: 'Basic Model',
description:
'This model has standard permissions and settings. Use this model for testing generic new features and services.',
organisation: 'Example',
state: 'Development',
collaborators: [
{
entity: 'user:user',
Expand Down
5 changes: 4 additions & 1 deletion backend/src/services/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,10 @@ export async function updateModelCard(
return revision
}

export type UpdateModelParams = Pick<ModelInterface, 'name' | 'description' | 'visibility' | 'collaborators'> & {
export type UpdateModelParams = Pick<
ModelInterface,
'name' | 'description' | 'visibility' | 'collaborators' | 'state' | 'organisation'
> & {
settings: Partial<ModelInterface['settings']>
}
export async function updateModel(user: UserInterface, modelId: string, modelDiff: Partial<UpdateModelParams>) {
Expand Down
5 changes: 5 additions & 0 deletions backend/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,9 @@ export interface UiConfig {
helpPopoverText: {
manualEntryAccess: string
}

modelDetails: {
organisations: string[]
states: string[]
}
}
4 changes: 4 additions & 0 deletions backend/src/utils/__mocks__/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ const config: PartialDeep<Config> = {
inference: {
enabled: true,
},
modelDetails: {
organisations: ['My Organisation'],
states: ['Development', 'Review', 'Production'],
},
},
}

Expand Down
7 changes: 6 additions & 1 deletion frontend/actions/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ export async function postModel(form: EntryForm) {

export async function patchModel(
id: string,
delta: Partial<Pick<EntryInterface, 'name' | 'description' | 'collaborators' | 'visibility' | 'settings'>>,
delta: Partial<
Pick<
EntryInterface,
'name' | 'description' | 'collaborators' | 'visibility' | 'settings' | 'organisation' | 'state'
>
>,
) {
return fetch(`/api/v2/model/${id}`, {
method: 'PATCH',
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/entry/CreateEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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 EntryOrganisationInput from 'src/entry/EntryOrganisationInput'
import EntryAccessInput from 'src/entry/settings/EntryAccessInput'
import SourceModelInput from 'src/entry/SourceModelnput'
import MessageAlert from 'src/MessageAlert'
Expand Down Expand Up @@ -53,6 +54,7 @@ export default function CreateEntry({ createEntryKind, onBackClick }: CreateEntr

const [name, setName] = useState('')
const [sourceModelId, setSourceModelId] = useState('')
const [organisation, setOrganisation] = useState<string>('')
const [description, setDescription] = useState('')
const [visibility, setVisibility] = useState<EntryForm['visibility']>(EntryVisibility.Public)
const [collaborators, setCollaborators] = useState<CollaboratorEntry[]>(
Expand All @@ -73,13 +75,14 @@ export default function CreateEntry({ createEntryKind, onBackClick }: CreateEntr
[name, description, createEntryKind, sourceModelId],
)

async function handleSubmit(event: FormEvent<HTMLFormElement>) {
async function handleSubmit(event: FormEvent) {
event.preventDefault()
setLoading(true)
setErrorMessage('')

const formData: EntryForm = {
name,
organisation,
kind: entryKind,
description,
visibility,
Expand Down Expand Up @@ -191,6 +194,7 @@ export default function CreateEntry({ createEntryKind, onBackClick }: CreateEntr
</Typography>
<Stack spacing={2} direction={{ xs: 'column', sm: 'row' }}>
<EntryNameInput autoFocus value={name} kind={entryKind} onChange={(value) => setName(value)} />
<EntryOrganisationInput value={organisation} onChange={(value) => setOrganisation(value)} />
{createEntryKind === CreateEntryKind.MIRRORED_MODEL && (
<SourceModelInput onChange={(value) => setSourceModelId(value)} value={sourceModelId} />
)}
Expand Down
58 changes: 58 additions & 0 deletions frontend/src/entry/EntryOrganisationInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MenuItem, Select, SelectChangeEvent } from '@mui/material'
import { useGetUiConfig } from 'actions/uiConfig'
import { useMemo } from 'react'
import LabelledInput from 'src/common/LabelledInput'
import Loading from 'src/common/Loading'
import MessageAlert from 'src/MessageAlert'

const htmlId = 'entry-organisation-input'

type EntryOrganisationInputProps = {
value: string
onChange: (value: string) => void
}

export default function EntryOrganisationInput({ value, onChange }: EntryOrganisationInputProps) {
const { uiConfig, isUiConfigLoading, isUiConfigError } = useGetUiConfig()

const handleChange = (event: SelectChangeEvent) => {
onChange(event.target.value)
}

const organisationOptions = useMemo(
() =>
uiConfig
? [
<MenuItem value={''} key='unset'>
<em>Unset</em>
</MenuItem>,
...uiConfig.modelDetails.organisations.map((organisationItem) => (
<MenuItem value={organisationItem} key={organisationItem}>
{organisationItem}
</MenuItem>
)),
]
: [],
[uiConfig],
)

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

if (!uiConfig || isUiConfigLoading) {
return <Loading />
}

if (uiConfig.modelDetails.organisations.length === 0) {
return <></>
}

return (
<LabelledInput fullWidth label='Organisation' htmlFor={htmlId}>
<Select size='small' value={value} onChange={handleChange} id={htmlId}>
{organisationOptions}
</Select>
</LabelledInput>
)
}
58 changes: 58 additions & 0 deletions frontend/src/entry/EntryStateInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MenuItem, Select, SelectChangeEvent } from '@mui/material'
import { useGetUiConfig } from 'actions/uiConfig'
import { useMemo } from 'react'
import LabelledInput from 'src/common/LabelledInput'
import Loading from 'src/common/Loading'
import MessageAlert from 'src/MessageAlert'

const htmlId = 'entry-state-input'

type EntryStateInputProps = {
value: string
onChange: (value: string) => void
}

export default function EntryStateInput({ value, onChange }: EntryStateInputProps) {
const { uiConfig, isUiConfigLoading, isUiConfigError } = useGetUiConfig()

const handleChange = (event: SelectChangeEvent) => {
onChange(event.target.value)
}

const stateOptions = useMemo(
() =>
uiConfig
? [
<MenuItem value={''} key='unset'>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistency- here you use the term 'unset' and for organisation you use 'no organisation'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My vote would be "None" for both

<em>Unset</em>
</MenuItem>,
...uiConfig.modelDetails.states.map((stateItem) => (
<MenuItem value={stateItem} key={stateItem}>
{stateItem}
</MenuItem>
)),
]
: [],
[uiConfig],
)

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

if (!uiConfig || isUiConfigLoading) {
return <Loading />
}

if (uiConfig.modelDetails.states.length === 0) {
return <></>
}

return (
<LabelledInput fullWidth label='Model State' htmlFor={htmlId}>
<Select size='small' value={value} onChange={handleChange} id={htmlId}>
{stateOptions}
</Select>
</LabelledInput>
)
}
8 changes: 8 additions & 0 deletions frontend/src/entry/settings/EntryDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { patchModel } from 'actions/model'
import { FormEvent, useMemo, useState } from 'react'
import EntryDescriptionInput from 'src/entry/EntryDescriptionInput'
import EntryNameInput from 'src/entry/EntryNameInput'
import EntryOrganisationInput from 'src/entry/EntryOrganisationInput'
import EntryStateInput from 'src/entry/EntryStateInput'
import useNotification from 'src/hooks/useNotification'
import MessageAlert from 'src/MessageAlert'
import { EntryInterface, EntryKindLabel, UpdateEntryForm } from 'types/types'
Expand All @@ -17,6 +19,8 @@ type EntryDetailsProps = {

export default function EntryDetails({ entry }: EntryDetailsProps) {
const [name, setName] = useState(entry.name)
const [organisation, setOrganisation] = useState(entry.organisation || '')
const [state, setState] = useState(entry.state || '')
const [description, setDescription] = useState(entry.description)
const [visibility, setVisibility] = useState<UpdateEntryForm['visibility']>(entry.visibility)
const [isLoading, setIsLoading] = useState(false)
Expand All @@ -42,6 +46,8 @@ export default function EntryDetails({ entry }: EntryDetailsProps) {
name,
description,
visibility,
organisation: organisation || '',
state: state || '',
}
const response = await patchModel(entry.id, formData)

Expand Down Expand Up @@ -93,7 +99,9 @@ export default function EntryDetails({ entry }: EntryDetailsProps) {
{`${toTitleCase(EntryKindLabel[entry.kind])} Details`}
</Typography>
<EntryNameInput autoFocus value={name} kind={entry.kind} onChange={(value) => setName(value)} />
<EntryOrganisationInput value={organisation} onChange={(value) => setOrganisation(value)} />
<EntryDescriptionInput value={description} onChange={(value) => setDescription(value)} />
<EntryStateInput value={state} onChange={(value) => setState(value)} />
</>
<Divider />
<>
Expand Down
9 changes: 9 additions & 0 deletions frontend/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export interface UiConfig {
helpPopoverText: {
manualEntryAccess: string
}

modelDetails: {
organisations: string[]
states: string[]
}
}

export interface FileInterface {
Expand Down Expand Up @@ -402,6 +407,8 @@ export type CreateEntryKindKeys = (typeof CreateEntryKind)[keyof typeof CreateEn
export interface EntryInterface {
id: string
name: string
organisation?: string
state?: string
kind: EntryKindKeys
description: string
settings: {
Expand Down Expand Up @@ -433,6 +440,8 @@ export interface EntryForm {
destinationModelId?: string
}
}
organisation?: string
state?: string
}

export type UpdateEntryForm = Omit<EntryForm, 'kind' | 'collaborators'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ data:
helpPopoverText: {
manualEntryAccess: '{{ .Values.config.ui.helpPopoverText.manualEntryAccess }}'
},

modelDetails: {
organisations: {{ toJson .Values.config.ui.modelDetails.organisations }},
states: {{ toJson .Values.config.ui.modelDetails.states }},
},
},

connectors: {
Expand Down
4 changes: 4 additions & 0 deletions infrastructure/helm/bailo/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ config:
helpPopoverText:
manualEntryAccess: ''

modelDetails:
organisations: []
states: []

smtp:
#host: 'mail' #service name
port: 1025
Expand Down
Loading
Loading