From 2e2215fd77e5179feef5a0dd2a3a02809e273388 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Fri, 17 May 2024 13:08:18 +0000 Subject: [PATCH 1/6] Updated token actions to an autocomplete --- frontend/actions/user.ts | 4 +- .../settings/personal-access-tokens/new.tsx | 164 +++++++++++++----- frontend/types/types.ts | 34 +++- 3 files changed, 155 insertions(+), 47 deletions(-) diff --git a/frontend/actions/user.ts b/frontend/actions/user.ts index d8f881ab4..0762e4f65 100644 --- a/frontend/actions/user.ts +++ b/frontend/actions/user.ts @@ -1,7 +1,7 @@ import qs from 'querystring' import { UserInformation } from 'src/common/UserDisplay' import useSWR from 'swr' -import { EntityObject, EntryInterface, TokenActionsKeys, TokenInterface, TokenScopeKeys, User } from 'types/types' +import { EntityObject, EntryInterface, TokenActionKeys, TokenInterface, TokenScopeKeys, User } from 'types/types' import { ErrorInfo, fetcher } from '../utils/fetcher' @@ -77,7 +77,7 @@ export function postUserToken( description: string, scope: TokenScopeKeys, modelIds: EntryInterface['id'][], - actions: TokenActionsKeys[], + actions: TokenActionKeys[], ) { return fetch('/api/v2/user/tokens', { method: 'post', diff --git a/frontend/pages/settings/personal-access-tokens/new.tsx b/frontend/pages/settings/personal-access-tokens/new.tsx index ac7199c10..12c12a89c 100644 --- a/frontend/pages/settings/personal-access-tokens/new.tsx +++ b/frontend/pages/settings/personal-access-tokens/new.tsx @@ -2,13 +2,16 @@ import { ArrowBack } from '@mui/icons-material' import { LoadingButton } from '@mui/lab' import { Autocomplete, + AutocompleteChangeDetails, + AutocompleteChangeReason, + AutocompleteRenderGetTagProps, Button, Card, Checkbox, + Chip, Container, FormControl, FormControlLabel, - Grid, Stack, TextField, Typography, @@ -21,16 +24,48 @@ import Title from 'src/common/Title' import Link from 'src/Link' import MessageAlert from 'src/MessageAlert' import TokenDialog from 'src/settings/authentication/TokenDialog' -import { TokenActions, TokenActionsKeys, TokenInterface, TokenScope } from 'types/types' +import { TokenAction, TokenActionKeys, TokenInterface, TokenScope } from 'types/types' import { getErrorMessage } from 'utils/fetcher' import { plural } from 'utils/stringUtils' +const [TokenReadAction, TokenWriteAction] = Object.values(TokenAction).reduce[]>( + ([readActions, writeActions], action) => { + let groupedActions = [readActions, writeActions] + const [name, kind] = action.split(':') + + if (kind === 'read') { + groupedActions = [{ ...readActions, [name]: action }, writeActions] + } + if (kind === 'write') { + groupedActions = [readActions, { ...writeActions, [name]: action }] + } + + return groupedActions + }, + [{}, {}], +) + +const isWriteAction = (action: TokenActionKeys) => { + return Object.values(TokenWriteAction).includes(action) +} + +const isReadAction = (action: TokenActionKeys) => { + return Object.values(TokenReadAction).includes(action) +} + +const getActionName = (action: TokenActionKeys) => { + return action.split(':')[0] +} + +const actionOptions = Object.values(TokenAction) + export default function NewToken() { const theme = useTheme() const [description, setDescription] = useState('') - const [isAllModels, setIsAllModels] = useState(false) + const [isAllModels, setIsAllModels] = useState(true) const [selectedModels, setSelectedModels] = useState([]) - const [selectedActions, setSelectedActions] = useState([]) + const [isAllActions, setIsAllActions] = useState(true) + const [selectedActions, setSelectedActions] = useState(actionOptions) const [isLoading, setIsLoading] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [token, setToken] = useState() @@ -48,42 +83,33 @@ export default function NewToken() { [description, isAllModels, selectedModels.length, selectedActions.length], ) - const handleSelectedActionsChange = useCallback( - (action: TokenActionsKeys, checked: boolean) => { - if (checked) { - setSelectedActions([...selectedActions, action]) - } else { - const foundIndex = selectedActions.findIndex((selectedRepository) => selectedRepository === action) - if (foundIndex >= 0) { - const updatedSelectedActions = [...selectedActions] - updatedSelectedActions.splice(foundIndex, 1) - setSelectedActions(updatedSelectedActions) + const renderActionTags = useCallback( + (value: TokenActionKeys[], getTagProps: AutocompleteRenderGetTagProps) => + value.map((option, index) => { + const actionName = getActionName(option) + const isRequired = + option === TokenAction.MODEL_READ || + (isReadAction(option) && selectedActions.includes(TokenWriteAction[actionName])) + + // overrideProps is used to disable delete functionality for model:read and any selected + // read actions with a corresponding selected write action + const overrideProps = { + ...(isRequired && { + onDelete: undefined, + }), } - } - }, - [selectedActions], - ) - const actionCheckboxes = useMemo( - () => - Object.values(TokenActions).map((action) => ( - - - handleSelectedActionsChange(action, checked)} - data-test={`${action.replace(':', '')}ActionCheckbox`} - /> - } - label={action} - /> - - - )), - [handleSelectedActionsChange, selectedActions], + return ( + + ) + }), + [selectedActions], ) const handleDescriptionChange = (event: ChangeEvent) => { @@ -95,10 +121,42 @@ export default function NewToken() { setSelectedModels([]) } - const handleSelectedModelsChange = (_: SyntheticEvent, value: EntrySearchResult[]) => { + const handleSelectedModelsChange = (_event: SyntheticEvent, value: EntrySearchResult[]) => { setSelectedModels(value) } + const handleAllActionsChange = (_event: ChangeEvent, checked: boolean) => { + setIsAllActions(checked) + setSelectedActions(checked ? actionOptions : [TokenAction.MODEL_READ]) + } + + const handleSelectedActionsChange = ( + _event: SyntheticEvent, + value: TokenActionKeys[], + reason: AutocompleteChangeReason, + details?: AutocompleteChangeDetails, + ) => { + if (reason === 'clear') { + setIsAllActions(false) + setSelectedActions([TokenAction.MODEL_READ]) + return + } + + const updatedValue = [...value] + + // If the selected option is a write action, ensure the corresponding + // read action is also selected + if (reason === 'selectOption' && details && isWriteAction(details.option)) { + const actionName = getActionName(details.option) + if (!updatedValue.includes(TokenReadAction[actionName])) { + updatedValue.push(TokenReadAction[actionName]) + } + } + + setIsAllActions(updatedValue.length === actionOptions.length) + setSelectedActions(updatedValue) + } + const handleSubmit = async () => { setIsLoading(true) const scope = isAllModels ? TokenScope.All : TokenScope.Models @@ -183,7 +241,33 @@ export default function NewToken() { Actions * - {actionCheckboxes} + + + + } + label='All' + /> + + `+${plural(more, 'action')}`} + onChange={handleSelectedActionsChange} + renderInput={(params) => } + renderTags={renderActionTags} + /> + - actions: Array + actions: Array accessKey: string secretKey: string deleted: boolean From b5d56dea8be23a6ed5ff14ac305a986d8277ffa1 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Fri, 17 May 2024 14:25:32 +0000 Subject: [PATCH 2/6] Updated push image registry tests --- frontend/cypress/e2e/bailo/push-image-registry.cy.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/cypress/e2e/bailo/push-image-registry.cy.ts b/frontend/cypress/e2e/bailo/push-image-registry.cy.ts index 3c5fc9b5d..d2e087fd3 100644 --- a/frontend/cypress/e2e/bailo/push-image-registry.cy.ts +++ b/frontend/cypress/e2e/bailo/push-image-registry.cy.ts @@ -31,9 +31,6 @@ describe('Make and approve an access request', () => { cy.log('Navigating to token generation page') cy.visit(`/settings/personal-access-tokens/new`) cy.get('[data-test=tokenDescriptionTextField]').type('This token works for all models') - cy.get('[data-test=allModelsCheckbox]').click() - cy.get('[data-test=imagereadActionCheckbox]').click() - cy.get('[data-test=filereadActionCheckbox]').click() cy.get('[data-test=generatePersonalAccessTokenButton]').click() cy.log('Saving access key and secret key') From d0386dabd31cc7e9e65b695c6c2a65d05ba993a4 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Mon, 20 May 2024 13:05:10 +0000 Subject: [PATCH 3/6] Removed webhook and inference actions and re-enabled tests --- frontend/cypress/e2e/bailo/push-image-registry.cy.ts | 5 ++--- frontend/types/types.ts | 6 ------ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/frontend/cypress/e2e/bailo/push-image-registry.cy.ts b/frontend/cypress/e2e/bailo/push-image-registry.cy.ts index d2e087fd3..2490504c0 100644 --- a/frontend/cypress/e2e/bailo/push-image-registry.cy.ts +++ b/frontend/cypress/e2e/bailo/push-image-registry.cy.ts @@ -40,8 +40,7 @@ describe('Make and approve an access request', () => { cy.get('[data-test=secretKeyText]').invoke('text').as('secretKey') }) - // Temporarily disabled until token UI changes go in. - it.skip('can push and pull to the registry', function () { + it('can push and pull to the registry', function () { cy.log('Running all the docker commands to push an image') cy.exec(`docker login ${registryUrl} -u ${this.accessKey} -p ${this.secretKey}`, { timeout: 60000 }) cy.exec(`docker build --tag ${testModelImage} cypress/fixtures/docker-image`, { timeout: 60000 }) @@ -51,7 +50,7 @@ describe('Make and approve an access request', () => { cy.exec(`docker push ${registryUrl}/${modelUuidForRegistry}/${testModelImage}:1`, { timeout: 60000 }) }) - it.skip('can select the image when drafting a release', () => { + it('can select the image when drafting a release', () => { cy.log('Navigating to the model page and then to the releases tab') cy.visit(`/model/${modelUuidForRegistry}`) cy.contains(modelNameForRegistry) diff --git a/frontend/types/types.ts b/frontend/types/types.ts index f5324e434..5ef6e6a0d 100644 --- a/frontend/types/types.ts +++ b/frontend/types/types.ts @@ -185,18 +185,12 @@ export const TokenAction = { ACCESS_REQUEST_READ: 'access_request:read', ACCESS_REQUEST_WRITE: 'access_request:write', - WEBHOOK_READ: 'webhook:read', - WEBHOOK_WRITE: 'webhook:write', - FILE_READ: 'file:read', FILE_WRITE: 'file:write', IMAGE_READ: 'image:read', IMAGE_WRITE: 'image:write', - INFERENCE_READ: 'inference:read', - INFERENCE_WRITE: 'inference:write', - SCHEMA_READ: 'schema:read', SCHEMA_WRITE: 'schema:write', From 7d880f7063927eba28c187e1081037e60d2c4f78 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Mon, 20 May 2024 14:06:35 +0000 Subject: [PATCH 4/6] Route user back to settings page on token dialog close --- .../settings/authentication/TokenDialog.tsx | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/frontend/src/settings/authentication/TokenDialog.tsx b/frontend/src/settings/authentication/TokenDialog.tsx index d0b332dff..378ef4e45 100644 --- a/frontend/src/settings/authentication/TokenDialog.tsx +++ b/frontend/src/settings/authentication/TokenDialog.tsx @@ -33,15 +33,10 @@ type TokenDialogProps = { } export default function TokenDialog({ token }: TokenDialogProps) { - const [open, setOpen] = useState(false) const router = useRouter() const { tab } = router.query const [tokenCategory, setTokenCategory] = useState(TokenCategory.PERSONAL_ACCESS) - useEffect(() => { - if (token) setOpen(true) - }, [token]) - useEffect(() => { if (isTokenCategory(tab)) { setTokenCategory(tab ?? TokenCategory.PERSONAL_ACCESS) @@ -49,7 +44,7 @@ export default function TokenDialog({ token }: TokenDialogProps) { }, [tab, setTokenCategory]) const handleClose = () => { - setOpen(false) + router.push('/settings?tab=authentication') } const handleListItemClick = (category: TokenCategoryKeys) => { @@ -57,16 +52,7 @@ export default function TokenDialog({ token }: TokenDialogProps) { } return ( - { - if (reason !== 'backdropClick') setOpen(false) - }} - PaperProps={{ sx: { height: '90vh' } }} - > + Token Created Date: Mon, 20 May 2024 14:07:57 +0000 Subject: [PATCH 5/6] Renamed token actions heading --- frontend/pages/settings/personal-access-tokens/new.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/settings/personal-access-tokens/new.tsx b/frontend/pages/settings/personal-access-tokens/new.tsx index 12c12a89c..6d8d6d8aa 100644 --- a/frontend/pages/settings/personal-access-tokens/new.tsx +++ b/frontend/pages/settings/personal-access-tokens/new.tsx @@ -239,7 +239,7 @@ export default function NewToken() { - Actions * + Permissions * From a778cb854466602d4ef3e1ac15617ca680e322c2 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Tue, 21 May 2024 09:23:14 +0000 Subject: [PATCH 6/6] Typed token action kind and improved getActionName logic --- frontend/pages/settings/personal-access-tokens/new.tsx | 9 +++++---- frontend/types/types.ts | 7 +++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/pages/settings/personal-access-tokens/new.tsx b/frontend/pages/settings/personal-access-tokens/new.tsx index 6d8d6d8aa..6091122cd 100644 --- a/frontend/pages/settings/personal-access-tokens/new.tsx +++ b/frontend/pages/settings/personal-access-tokens/new.tsx @@ -24,7 +24,7 @@ import Title from 'src/common/Title' import Link from 'src/Link' import MessageAlert from 'src/MessageAlert' import TokenDialog from 'src/settings/authentication/TokenDialog' -import { TokenAction, TokenActionKeys, TokenInterface, TokenScope } from 'types/types' +import { TokenAction, TokenActionKeys, TokenActionKind, TokenInterface, TokenScope } from 'types/types' import { getErrorMessage } from 'utils/fetcher' import { plural } from 'utils/stringUtils' @@ -33,10 +33,10 @@ const [TokenReadAction, TokenWriteAction] = Object.values(TokenAction).reduce { } const getActionName = (action: TokenActionKeys) => { - return action.split(':')[0] + const [name, _kind] = action.split(':') + return name } const actionOptions = Object.values(TokenAction) diff --git a/frontend/types/types.ts b/frontend/types/types.ts index 5ef6e6a0d..de8dbcbd2 100644 --- a/frontend/types/types.ts +++ b/frontend/types/types.ts @@ -175,6 +175,13 @@ export const TokenScope = { export type TokenScopeKeys = (typeof TokenScope)[keyof typeof TokenScope] +export const TokenActionKind = { + READ: 'read', + WRITE: 'write', +} + +export type TokenActionKindKeys = (typeof TokenActionKind)[keyof typeof TokenActionKind] + export const TokenAction = { MODEL_READ: 'model:read', MODEL_WRITE: 'model:write',