From 84d2c735ea44f74731a6b9fcb96ded6bc2c8fd15 Mon Sep 17 00:00:00 2001 From: araddcc002 Date: Mon, 21 Oct 2024 09:30:47 +0000 Subject: [PATCH 01/11] wip# --- frontend/actions/model.ts | 18 ++++++- frontend/src/common/Restricted.tsx | 26 ++++++++++ .../src/contexts/userPermissionsContext.ts | 9 ++++ frontend/src/hooks/UserPermissionsHook.ts | 48 +++++++++++++++++++ frontend/types/types.ts | 28 +++++++++++ 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 frontend/src/common/Restricted.tsx create mode 100644 frontend/src/contexts/userPermissionsContext.ts create mode 100644 frontend/src/hooks/UserPermissionsHook.ts diff --git a/frontend/actions/model.ts b/frontend/actions/model.ts index 3eb6a8868..c2fa1bb7c 100644 --- a/frontend/actions/model.ts +++ b/frontend/actions/model.ts @@ -1,7 +1,7 @@ import qs from 'querystring' import useSWR from 'swr' -import { EntryForm, EntryInterface, EntryKindKeys, ModelImage, Role } from '../types/types' +import { EntryForm, EntryInterface, EntryKindKeys, ModelImage, Role, UserPermissions } from '../types/types' import { ErrorInfo, fetcher } from '../utils/fetcher' const emptyModelList = [] @@ -151,3 +151,19 @@ export async function postModelExportToS3(id: string, modelExport: ModelExportRe body: JSON.stringify(modelExport), }) } + +export function useGetCurrentUserPermissionsForEntry(entryId: string) { + const { data, isLoading, error, mutate } = useSWR< + { + permissions: UserPermissions + }, + ErrorInfo + >(entryId ? `/api/v2/model/${entryId}/permissions/mine` : null, fetcher) + + return { + mutateUserPermissions: mutate, + userPermissions: data ? data.permissions : undefined, + isUserPermissionsLoading: isLoading, + isUserPermissionsError: error, + } +} diff --git a/frontend/src/common/Restricted.tsx b/frontend/src/common/Restricted.tsx new file mode 100644 index 000000000..930d1c1bc --- /dev/null +++ b/frontend/src/common/Restricted.tsx @@ -0,0 +1,26 @@ +import { Tooltip } from '@mui/material' +import { ReactElement, useContext, useMemo } from 'react' +import UserPermissionsContext from 'src/contexts/userPermissionsContext' +import { RestrictedActionKeys } from 'types/types' + +type RestrictedProps = { + action: RestrictedActionKeys + fallback?: ReactElement + children: ReactElement +} + +export default function Restricted({ action, fallback, children }: RestrictedProps) { + const { userPermissions } = useContext(UserPermissionsContext) + + const permission = useMemo(() => userPermissions[action], [action, userPermissions]) + + if (permission.hasPermission) { + return <>{children} + } + + if (fallback) { + return {fallback} + } + + return null +} diff --git a/frontend/src/contexts/userPermissionsContext.ts b/frontend/src/contexts/userPermissionsContext.ts new file mode 100644 index 000000000..0e07404c1 --- /dev/null +++ b/frontend/src/contexts/userPermissionsContext.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react' +import { defaultPermissions, UserPermissionsHook } from 'src/hooks/UserPermissionsHook' + +const UserPermissionsContext = createContext({ + userPermissions: defaultPermissions, + setUserPermissions: () => undefined, +}) + +export default UserPermissionsContext diff --git a/frontend/src/hooks/UserPermissionsHook.ts b/frontend/src/hooks/UserPermissionsHook.ts new file mode 100644 index 000000000..cd3925402 --- /dev/null +++ b/frontend/src/hooks/UserPermissionsHook.ts @@ -0,0 +1,48 @@ +import { useGetCurrentUserPermissionsForEntry } from 'actions/model' +import { useEffect, useState } from 'react' +import { PermissionDetail, UserPermissions } from 'types/types' + +export type UserPermissionsHook = { + userPermissions: UserPermissions + setUserPermissions: (permissions: UserPermissions) => void +} + +const defaultPermissionDetail: PermissionDetail = { + hasPermission: false, + info: 'Permission not set.', +} +export const defaultPermissions: UserPermissions = { + editEntry: defaultPermissionDetail, + editEntryCard: defaultPermissionDetail, + editAccessRequest: defaultPermissionDetail, + deleteAccessRequest: defaultPermissionDetail, + reviewAccessRequest: defaultPermissionDetail, + createRelease: defaultPermissionDetail, + editRelease: defaultPermissionDetail, + deleteRelease: defaultPermissionDetail, + reviewRelease: defaultPermissionDetail, + pushModelImage: defaultPermissionDetail, + createInferenceService: defaultPermissionDetail, + editInferenceService: defaultPermissionDetail, + exportMirroredModel: defaultPermissionDetail, +} + +export default function useUserPermissions(entryId: string): UserPermissionsHook { + const [userPermissions, setUserPermissions] = useState(defaultPermissions) + + const { + userPermissions: permissions, + isUserPermissionsLoading, + isUserPermissionsError, + } = useGetCurrentUserPermissionsForEntry(entryId) + + useEffect(() => { + if (permissions && !isUserPermissionsLoading && !isUserPermissionsError) { + setUserPermissions(permissions) + } + }, [isUserPermissionsError, isUserPermissionsLoading, permissions]) + return { + userPermissions, + setUserPermissions, + } +} diff --git a/frontend/types/types.ts b/frontend/types/types.ts index bf116d90d..f800c1ff5 100644 --- a/frontend/types/types.ts +++ b/frontend/types/types.ts @@ -549,3 +549,31 @@ export interface SuccessfulFileUpload { fileName: string fileId: string } + +export type PermissionDetail = + | { + hasPermission: true + info?: never + } + | { + hasPermission: false + info: string + } + +export interface UserPermissions { + editEntry: PermissionDetail + editEntryCard: PermissionDetail + editAccessRequest: PermissionDetail + deleteAccessRequest: PermissionDetail + reviewAccessRequest: PermissionDetail + createRelease: PermissionDetail + editRelease: PermissionDetail + deleteRelease: PermissionDetail + reviewRelease: PermissionDetail + pushModelImage: PermissionDetail + createInferenceService: PermissionDetail + editInferenceService: PermissionDetail + exportMirroredModel: PermissionDetail +} + +export type RestrictedActionKeys = keyof UserPermissions From 54cdc5dc4ddc44a4dac547a9059243233ccf4859 Mon Sep 17 00:00:00 2001 From: araddcc002 Date: Wed, 23 Oct 2024 08:30:04 +0000 Subject: [PATCH 02/11] wip - added context for permissions, added check for edit model --- frontend/pages/_app.tsx | 21 ++++--- frontend/pages/model/[modelId].tsx | 14 ++--- frontend/src/common/Restricted.tsx | 6 +- .../src/contexts/userPermissionsContext.ts | 2 +- frontend/src/entry/overview/FormEditPage.tsx | 56 +++++++++---------- frontend/src/entry/overview/Overview.tsx | 7 +-- frontend/src/hooks/UserPermissionsHook.ts | 7 ++- 7 files changed, 59 insertions(+), 54 deletions(-) diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index f3bfc5ceb..09cc1e5d8 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -12,6 +12,8 @@ import { AppProps } from 'next/app' import Head from 'next/head' import { SnackbarProvider } from 'notistack' import { useEffect } from 'react' +import UserPermissionsContext from 'src/contexts/userPermissionsContext' +import useUserPermissions from 'src/hooks/UserPermissionsHook' import Wrapper from 'src/Wrapper' import ThemeModeContext from '../src/contexts/themeModeContext' @@ -23,6 +25,7 @@ export default function MyApp(props: AppProps) { const { Component, pageProps } = props const themeModeValue = useThemeMode() const unsavedChangesValue = useUnsavedChanges() + const userPermissions = useUserPermissions() // This is set so that 'react-markdown-editor' respects the theme set by MUI. useEffect(() => { @@ -39,14 +42,16 @@ export default function MyApp(props: AppProps) { - - - - - - - - + + + + + + + + + + diff --git a/frontend/pages/model/[modelId].tsx b/frontend/pages/model/[modelId].tsx index 94cb785a4..3f793c736 100644 --- a/frontend/pages/model/[modelId].tsx +++ b/frontend/pages/model/[modelId].tsx @@ -2,10 +2,11 @@ import { useGetModel } from 'actions/model' import { useGetUiConfig } from 'actions/uiConfig' import { useGetCurrentUser } from 'actions/user' import { useRouter } from 'next/router' -import { useMemo } from 'react' +import { useContext, useMemo } from 'react' import Loading from 'src/common/Loading' import PageWithTabs, { PageTab } from 'src/common/PageWithTabs' import Title from 'src/common/Title' +import UserPermissionsContext from 'src/contexts/userPermissionsContext' import AccessRequests from 'src/entry/model/AccessRequests' import InferenceServices from 'src/entry/model/InferenceServices' import ModelImages from 'src/entry/model/ModelImages' @@ -23,6 +24,9 @@ export default function Model() { const { currentUser, isCurrentUserLoading, isCurrentUserError } = useGetCurrentUser() const { uiConfig, isUiConfigLoading, isUiConfigError } = useGetUiConfig() + const { setEntryId } = useContext(UserPermissionsContext) + setEntryId(modelId || '') + const currentUserRoles = useMemo(() => getCurrentUserRoles(model, currentUser), [model, currentUser]) const [isReadOnly, requiredRolesText] = useMemo(() => { @@ -37,13 +41,7 @@ export default function Model() { { title: 'Overview', path: 'overview', - view: ( - - ), + view: , }, { title: 'Releases', diff --git a/frontend/src/common/Restricted.tsx b/frontend/src/common/Restricted.tsx index 930d1c1bc..4a39c4e2c 100644 --- a/frontend/src/common/Restricted.tsx +++ b/frontend/src/common/Restricted.tsx @@ -19,7 +19,11 @@ export default function Restricted({ action, fallback, children }: RestrictedPro } if (fallback) { - return {fallback} + return ( + +
{fallback}
+
+ ) } return null diff --git a/frontend/src/contexts/userPermissionsContext.ts b/frontend/src/contexts/userPermissionsContext.ts index 0e07404c1..72eb585ad 100644 --- a/frontend/src/contexts/userPermissionsContext.ts +++ b/frontend/src/contexts/userPermissionsContext.ts @@ -3,7 +3,7 @@ import { defaultPermissions, UserPermissionsHook } from 'src/hooks/UserPermissio const UserPermissionsContext = createContext({ userPermissions: defaultPermissions, - setUserPermissions: () => undefined, + setEntryId: () => undefined, }) export default UserPermissionsContext diff --git a/frontend/src/entry/overview/FormEditPage.tsx b/frontend/src/entry/overview/FormEditPage.tsx index de52ce057..aa46130af 100644 --- a/frontend/src/entry/overview/FormEditPage.tsx +++ b/frontend/src/entry/overview/FormEditPage.tsx @@ -2,14 +2,15 @@ import EditIcon from '@mui/icons-material/Edit' import HistoryIcon from '@mui/icons-material/History' import PersonIcon from '@mui/icons-material/Person' import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf' -import { Box, Button, ListItemIcon, ListItemText, Menu, MenuItem, Stack, Tooltip, Typography } from '@mui/material' +import { Box, Button, ListItemIcon, ListItemText, Menu, MenuItem, Stack, Typography } from '@mui/material' import { useGetModel } from 'actions/model' import { putModelCard } from 'actions/modelCard' import { useGetSchema } from 'actions/schema' -import React from 'react' -import { useContext, useEffect, useMemo, useState } from 'react' +import React, { useMemo } from 'react' +import { useContext, useEffect, useState } from 'react' import CopyToClipboardButton from 'src/common/CopyToClipboardButton' import Loading from 'src/common/Loading' +import Restricted from 'src/common/Restricted' import TextInputDialog from 'src/common/TextInputDialog' import UnsavedChangesContext from 'src/contexts/unsavedChangesContext' import EntryCardHistoryDialog from 'src/entry/overview/EntryCardHistoryDialog' @@ -21,13 +22,10 @@ import useNotification from 'src/hooks/useNotification' import MessageAlert from 'src/MessageAlert' import { EntryCardKindLabel, EntryInterface, SplitSchemaNoRender } from 'types/types' import { getStepsData, getStepsFromSchema } from 'utils/formUtils' -import { getRequiredRolesText, hasRole } from 'utils/roles' type FormEditPageProps = { entry: EntryInterface - readOnly?: boolean - currentUserRoles: string[] } -export default function FormEditPage({ entry, currentUserRoles, readOnly = false }: FormEditPageProps) { +export default function FormEditPage({ entry }: FormEditPageProps) { const [isEdit, setIsEdit] = useState(false) const [splitSchema, setSplitSchema] = useState({ reference: '', steps: [] }) const [errorMessage, setErrorMessage] = useState('') @@ -51,10 +49,6 @@ export default function FormEditPage({ entry, currentUserRoles, readOnly = false const sendNotification = useNotification() const { setUnsavedChanges } = useContext(UnsavedChangesContext) - const [canEdit, requiredRolesText] = useMemo(() => { - const validRoles = ['owner', 'mtr', 'msro', 'contributor'] - return [hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) async function onSubmit() { if (schema) { setErrorMessage('') @@ -110,6 +104,18 @@ export default function FormEditPage({ entry, currentUserRoles, readOnly = false }) } } + + const editMenuItemContent = useMemo(() => { + return ( + <> + + + + {`Edit ${EntryCardKindLabel[entry.kind]}`} + + ) + }, [entry.kind]) + if (isSchemaError) { return } @@ -176,23 +182,17 @@ export default function FormEditPage({ entry, currentUserRoles, readOnly = false View History - {!readOnly && ( - - { - handleActionButtonClose() - setIsEdit(!isEdit) - }} - data-test='editEntryCardButton' - > - - - - {`Edit ${EntryCardKindLabel[entry.kind]}`} - - - )} + {editMenuItemContent}}> + { + handleActionButtonClose() + setIsEdit(!isEdit) + }} + data-test='editEntryCardButton' + > + {editMenuItemContent} + + )} diff --git a/frontend/src/entry/overview/Overview.tsx b/frontend/src/entry/overview/Overview.tsx index a5d20417a..43b007b54 100644 --- a/frontend/src/entry/overview/Overview.tsx +++ b/frontend/src/entry/overview/Overview.tsx @@ -15,10 +15,9 @@ type OverviewPageKeys = (typeof OverviewPage)[keyof typeof OverviewPage] type OverviewProps = { entry: EntryInterface readOnly?: boolean - currentUserRoles: string[] } -export default function Overview({ entry, currentUserRoles, readOnly = false }: OverviewProps) { +export default function Overview({ entry, readOnly = false }: OverviewProps) { const page: OverviewPageKeys = useMemo( () => (entry.card && entry.card.schemaId ? OverviewPage.FORM : OverviewPage.TEMPLATE), [entry.card], @@ -32,9 +31,7 @@ export default function Overview({ entry, currentUserRoles, readOnly = false }: ) : ( {page === OverviewPage.TEMPLATE && } - {page === OverviewPage.FORM && ( - - )} + {page === OverviewPage.FORM && } ) } diff --git a/frontend/src/hooks/UserPermissionsHook.ts b/frontend/src/hooks/UserPermissionsHook.ts index cd3925402..83eba0527 100644 --- a/frontend/src/hooks/UserPermissionsHook.ts +++ b/frontend/src/hooks/UserPermissionsHook.ts @@ -4,7 +4,7 @@ import { PermissionDetail, UserPermissions } from 'types/types' export type UserPermissionsHook = { userPermissions: UserPermissions - setUserPermissions: (permissions: UserPermissions) => void + setEntryId: (entryId: string) => void } const defaultPermissionDetail: PermissionDetail = { @@ -27,8 +27,9 @@ export const defaultPermissions: UserPermissions = { exportMirroredModel: defaultPermissionDetail, } -export default function useUserPermissions(entryId: string): UserPermissionsHook { +export default function useUserPermissions(): UserPermissionsHook { const [userPermissions, setUserPermissions] = useState(defaultPermissions) + const [entryId, setEntryId] = useState('') const { userPermissions: permissions, @@ -43,6 +44,6 @@ export default function useUserPermissions(entryId: string): UserPermissionsHook }, [isUserPermissionsError, isUserPermissionsLoading, permissions]) return { userPermissions, - setUserPermissions, + setEntryId, } } From b50c63a2ca1148ae27d8f9a0c6f0203b9a4ac432 Mon Sep 17 00:00:00 2001 From: araddcc002 Date: Wed, 30 Oct 2024 17:05:05 +0000 Subject: [PATCH 03/11] updated userpermission hooks to handle ui auth --- frontend/actions/accessRequest.ts | 21 +++++++- frontend/actions/model.ts | 14 ++--- frontend/pages/data-card/[dataCardId].tsx | 28 +++++----- frontend/pages/model/[modelId].tsx | 30 ++++------- .../access-request/[accessRequestId].tsx | 3 ++ .../inference/[image]/[tag]/settings.tsx | 15 +----- .../model/[modelId]/release/[semver].tsx | 4 +- frontend/src/Form/EditableFormHeading.tsx | 35 ++++++------- .../src/contexts/userPermissionsContext.ts | 5 +- .../src/entry/model/InferenceServices.tsx | 24 +++------ frontend/src/entry/model/ModelImages.tsx | 27 +++------- frontend/src/entry/model/Releases.tsx | 20 +++---- .../EditableAccessRequestForm.tsx | 40 +++----------- .../model/inferencing/EditableInference.tsx | 14 ++--- .../mirroredModels/ExportModelAgreement.tsx | 11 ++-- .../entry/model/releases/EditableRelease.tsx | 19 ++----- frontend/src/entry/settings/Settings.tsx | 14 +---- frontend/src/hooks/UserPermissionsHook.ts | 52 +++++++++++-------- frontend/src/hooks/useGetPermissions.ts | 19 +++++++ frontend/types/types.ts | 13 +++-- frontend/utils/roles.ts | 12 ----- 21 files changed, 179 insertions(+), 241 deletions(-) create mode 100644 frontend/src/hooks/useGetPermissions.ts diff --git a/frontend/actions/accessRequest.ts b/frontend/actions/accessRequest.ts index a502c0ab1..8d7e03720 100644 --- a/frontend/actions/accessRequest.ts +++ b/frontend/actions/accessRequest.ts @@ -1,5 +1,5 @@ import useSWR from 'swr' -import { AccessRequestInterface } from 'types/types' +import { AccessRequestInterface, AccessRequestUserPermissions } from 'types/types' import { ErrorInfo, fetcher } from 'utils/fetcher' const emptyAccessRequestList = [] @@ -66,3 +66,22 @@ export function postAccessRequestComment(modelId: string, accessRequestId: strin body: JSON.stringify({ comment }), }) } + +export function useGetCurrentUserPermissionsForAccessRequest(entryId?: string, accessRequestId?: string) { + const { data, isLoading, error, mutate } = useSWR< + { + permissions: AccessRequestUserPermissions + }, + ErrorInfo + >( + entryId && accessRequestId ? `/api/v2/model/${entryId}/access-request/${accessRequestId}/permissions/mine` : null, + fetcher, + ) + + return { + mutateAccessRequestUserPermissions: mutate, + accessRequestUserPermissions: data ? data.permissions : undefined, + isAccessRequestUserPermissionsLoading: isLoading, + isAccessRequestUserPermissionsError: error, + } +} diff --git a/frontend/actions/model.ts b/frontend/actions/model.ts index c2fa1bb7c..140f80b84 100644 --- a/frontend/actions/model.ts +++ b/frontend/actions/model.ts @@ -1,7 +1,7 @@ import qs from 'querystring' import useSWR from 'swr' -import { EntryForm, EntryInterface, EntryKindKeys, ModelImage, Role, UserPermissions } from '../types/types' +import { EntryForm, EntryInterface, EntryKindKeys, EntryUserPermissions, ModelImage, Role } from '../types/types' import { ErrorInfo, fetcher } from '../utils/fetcher' const emptyModelList = [] @@ -152,18 +152,18 @@ export async function postModelExportToS3(id: string, modelExport: ModelExportRe }) } -export function useGetCurrentUserPermissionsForEntry(entryId: string) { +export function useGetCurrentUserPermissionsForEntry(entryId?: string) { const { data, isLoading, error, mutate } = useSWR< { - permissions: UserPermissions + permissions: EntryUserPermissions }, ErrorInfo >(entryId ? `/api/v2/model/${entryId}/permissions/mine` : null, fetcher) return { - mutateUserPermissions: mutate, - userPermissions: data ? data.permissions : undefined, - isUserPermissionsLoading: isLoading, - isUserPermissionsError: error, + mutateEntryUserPermissions: mutate, + entryUserPermissions: data ? data.permissions : undefined, + isEntryUserPermissionsLoading: isLoading, + isEntryUserPermissionsError: error, } } diff --git a/frontend/pages/data-card/[dataCardId].tsx b/frontend/pages/data-card/[dataCardId].tsx index ce695da70..9fbf419cf 100644 --- a/frontend/pages/data-card/[dataCardId].tsx +++ b/frontend/pages/data-card/[dataCardId].tsx @@ -1,15 +1,15 @@ import { useGetModel } from 'actions/model' -import { useGetCurrentUser } from 'actions/user' import { useRouter } from 'next/router' -import { useMemo } from 'react' +import { useContext, useMemo } from 'react' import Loading from 'src/common/Loading' import PageWithTabs, { PageTab } from 'src/common/PageWithTabs' import Title from 'src/common/Title' +import UserPermissionsContext from 'src/contexts/userPermissionsContext' import Overview from 'src/entry/overview/Overview' import Settings from 'src/entry/settings/Settings' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' +import { useGetPermissions } from 'src/hooks/useGetPermissions' import { EntryKind } from 'types/types' -import { getCurrentUserRoles, getRequiredRolesText, hasRole } from 'utils/roles' export default function DataCard() { const router = useRouter() @@ -19,14 +19,11 @@ export default function DataCard() { isModelLoading: isDataCardLoading, isModelError: isDataCardError, } = useGetModel(dataCardId, EntryKind.DATA_CARD) - const { currentUser, isCurrentUserLoading, isCurrentUserError } = useGetCurrentUser() - const currentUserRoles = useMemo(() => getCurrentUserRoles(dataCard, currentUser), [dataCard, currentUser]) + useGetPermissions(dataCardId) + const { userPermissions } = useContext(UserPermissionsContext) - const [isReadOnly, requiredRolesText] = useMemo(() => { - const validRoles = ['owner'] - return [!hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) + const settingsPermission = useMemo(() => userPermissions['editEntry'], [userPermissions]) const tabs: PageTab[] = useMemo( () => @@ -35,30 +32,29 @@ export default function DataCard() { { title: 'Overview', path: 'overview', - view: , + view: , }, { title: 'Settings', path: 'settings', - disabled: isReadOnly, - disabledText: requiredRolesText, - view: , + disabled: !settingsPermission.hasPermission, + disabledText: settingsPermission.info, + view: , }, ] : [], - [currentUserRoles, dataCard, isReadOnly, requiredRolesText], + [dataCard, settingsPermission.hasPermission, settingsPermission.info], ) const error = MultipleErrorWrapper(`Unable to load data card page`, { isDataCardError, - isCurrentUserError, }) if (error) return error return ( <> - {(isDataCardLoading || isCurrentUserLoading) && <Loading />} + {isDataCardLoading && <Loading />} {dataCard && ( <PageWithTabs title={dataCard.name} diff --git a/frontend/pages/model/[modelId].tsx b/frontend/pages/model/[modelId].tsx index 3f793c736..6f3f59d62 100644 --- a/frontend/pages/model/[modelId].tsx +++ b/frontend/pages/model/[modelId].tsx @@ -14,8 +14,9 @@ import Releases from 'src/entry/model/Releases' import Overview from 'src/entry/overview/Overview' import Settings from 'src/entry/settings/Settings' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' +import { useGetPermissions } from 'src/hooks/useGetPermissions' import { EntryKind } from 'types/types' -import { getCurrentUserRoles, getRequiredRolesText, hasRole } from 'utils/roles' +import { getCurrentUserRoles } from 'utils/roles' export default function Model() { const router = useRouter() @@ -24,15 +25,12 @@ export default function Model() { const { currentUser, isCurrentUserLoading, isCurrentUserError } = useGetCurrentUser() const { uiConfig, isUiConfigLoading, isUiConfigError } = useGetUiConfig() - const { setEntryId } = useContext(UserPermissionsContext) - setEntryId(modelId || '') + useGetPermissions(modelId) + const { userPermissions } = useContext(UserPermissionsContext) const currentUserRoles = useMemo(() => getCurrentUserRoles(model, currentUser), [model, currentUser]) - const [isReadOnly, requiredRolesText] = useMemo(() => { - const validRoles = ['owner'] - return [!hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) + const settingsPermission = useMemo(() => userPermissions['editEntry'], [userPermissions]) const tabs: PageTab[] = useMemo( () => @@ -67,30 +65,24 @@ export default function Model() { { title: 'Registry', path: 'registry', - view: ( - <ModelImages - model={model} - currentUserRoles={currentUserRoles} - readOnly={!!model.settings.mirror?.sourceModelId} - /> - ), + view: <ModelImages model={model} readOnly={!!model.settings.mirror?.sourceModelId} />, }, { title: 'Inferencing', path: 'inferencing', - view: <InferenceServices model={model} currentUserRoles={currentUserRoles} />, + view: <InferenceServices model={model} />, hidden: !uiConfig.inference.enabled, }, { title: 'Settings', path: 'settings', - disabled: isReadOnly, - disabledText: requiredRolesText, - view: <Settings entry={model} currentUserRoles={currentUserRoles} />, + disabled: !settingsPermission.hasPermission, + disabledText: settingsPermission.info, + view: <Settings entry={model} />, }, ] : [], - [model, uiConfig, currentUserRoles, isReadOnly, requiredRolesText], + [model, uiConfig, currentUserRoles, settingsPermission.hasPermission, settingsPermission.info], ) function requestAccess() { diff --git a/frontend/pages/model/[modelId]/access-request/[accessRequestId].tsx b/frontend/pages/model/[modelId]/access-request/[accessRequestId].tsx index 99a209d6f..33d953ff7 100644 --- a/frontend/pages/model/[modelId]/access-request/[accessRequestId].tsx +++ b/frontend/pages/model/[modelId]/access-request/[accessRequestId].tsx @@ -12,6 +12,7 @@ import Title from 'src/common/Title' import EditableAccessRequestForm from 'src/entry/model/accessRequests/EditableAccessRequestForm' import ReviewBanner from 'src/entry/model/reviews/ReviewBanner' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' +import { useGetPermissions } from 'src/hooks/useGetPermissions' import Link from 'src/Link' import ReviewComments from 'src/reviews/ReviewComments' import { EntryKind } from 'types/types' @@ -23,6 +24,8 @@ export default function AccessRequest() { const [isEdit, setIsEdit] = useState(false) + useGetPermissions(modelId, accessRequestId) + const { accessRequest, isAccessRequestLoading, isAccessRequestError } = useGetAccessRequest(modelId, accessRequestId) const { reviews, isReviewsLoading, isReviewsError } = useGetReviewRequestsForModel({ modelId, diff --git a/frontend/pages/model/[modelId]/inference/[image]/[tag]/settings.tsx b/frontend/pages/model/[modelId]/inference/[image]/[tag]/settings.tsx index 3a0ec7983..a99c62057 100644 --- a/frontend/pages/model/[modelId]/inference/[image]/[tag]/settings.tsx +++ b/frontend/pages/model/[modelId]/inference/[image]/[tag]/settings.tsx @@ -1,37 +1,26 @@ import { ArrowBack } from '@mui/icons-material' import { Button, Card, Container, Divider, Link, Stack, Typography } from '@mui/material' import { useGetInference } from 'actions/inferencing' -import { useGetModel } from 'actions/model' -import { useGetCurrentUser } from 'actions/user' import { useRouter } from 'next/router' -import { useMemo } from 'react' import Loading from 'src/common/Loading' import Title from 'src/common/Title' import EditableInference from 'src/entry/model/inferencing/EditableInference' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' -import { EntryKind } from 'types/types' -import { getCurrentUserRoles } from 'utils/roles' export default function InferenceSettings() { const router = useRouter() const { modelId, image, tag }: { modelId?: string; image?: string; tag?: string } = router.query const { inference, isInferenceLoading, isInferenceError } = useGetInference(modelId, image, tag) - const { model, isModelLoading, isModelError } = useGetModel(modelId, EntryKind.MODEL) - const { currentUser, isCurrentUserLoading, isCurrentUserError } = useGetCurrentUser() - - const currentUserRoles = useMemo(() => getCurrentUserRoles(model, currentUser), [model, currentUser]) const error = MultipleErrorWrapper(`Unable to load inference settings page`, { isInferenceError, - isModelError, - isCurrentUserError, }) if (error) return error return ( <> <Title text={inference ? `${inference.image}:${inference.tag}` : 'Loading...'} /> - {!inference || isInferenceLoading || isModelLoading || isCurrentUserLoading ? ( + {!inference || isInferenceLoading ? ( <Loading /> ) : ( <Container maxWidth='md'> @@ -47,7 +36,7 @@ export default function InferenceSettings() { {inference ? `${inference.image}:${inference.tag}` : 'Loading...'} </Typography> </Stack> - {inference && <EditableInference inference={inference} currentUserRoles={currentUserRoles} />} + {inference && <EditableInference inference={inference} />} </Stack> </Card> </Container> diff --git a/frontend/pages/model/[modelId]/release/[semver].tsx b/frontend/pages/model/[modelId]/release/[semver].tsx index 14a32bc0d..cceae3940 100644 --- a/frontend/pages/model/[modelId]/release/[semver].tsx +++ b/frontend/pages/model/[modelId]/release/[semver].tsx @@ -12,6 +12,7 @@ import Title from 'src/common/Title' import EditableRelease from 'src/entry/model/releases/EditableRelease' import ReviewBanner from 'src/entry/model/reviews/ReviewBanner' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' +import { useGetPermissions } from 'src/hooks/useGetPermissions' import Link from 'src/Link' import ReviewComments from 'src/reviews/ReviewComments' import { EntryKind } from 'types/types' @@ -26,6 +27,8 @@ export default function Release() { const { release, isReleaseLoading, isReleaseError } = useGetRelease(modelId, semver) const { model, isModelLoading, isModelError } = useGetModel(modelId, EntryKind.MODEL) + useGetPermissions(modelId) + const { reviews, isReviewsLoading, isReviewsError } = useGetReviewRequestsForModel({ modelId, semver: semver || '', @@ -105,7 +108,6 @@ export default function Release() { {release && ( <EditableRelease release={release} - currentUserRoles={currentUserRoles} isEdit={isEdit} onIsEditChange={setIsEdit} readOnly={!!model?.settings.mirror?.sourceModelId} diff --git a/frontend/src/Form/EditableFormHeading.tsx b/frontend/src/Form/EditableFormHeading.tsx index 9735f1b89..1418a7332 100644 --- a/frontend/src/Form/EditableFormHeading.tsx +++ b/frontend/src/Form/EditableFormHeading.tsx @@ -1,20 +1,22 @@ import { LoadingButton } from '@mui/lab' -import { Button, Stack, Tooltip } from '@mui/material' +import { Button, Stack } from '@mui/material' import { ReactNode } from 'react' +import Restricted from 'src/common/Restricted' import MessageAlert from 'src/MessageAlert' +import { RestrictedActionKeys } from 'types/types' type EditableFormHeadingProps = { heading: ReactNode editButtonText: string isEdit: boolean isLoading: boolean - canUserEditOrDelete: boolean - actionButtonsTooltip: string onEdit: () => void onCancel: () => void onSubmit: () => void isRegistryError?: boolean onDelete?: () => void + editAction: RestrictedActionKeys + deleteAction?: RestrictedActionKeys errorMessage?: string deleteButtonText?: string showDeleteButton?: boolean @@ -31,13 +33,13 @@ export default function EditableFormHeading({ onCancel, onSubmit, onDelete, + editAction, + deleteAction, errorMessage = '', deleteButtonText = 'Delete', showDeleteButton = false, isRegistryError = false, readOnly = false, - canUserEditOrDelete = true, - actionButtonsTooltip = '', disableSaveButton = false, }: EditableFormHeadingProps) { return ( @@ -50,29 +52,26 @@ export default function EditableFormHeading({ > {heading} {!isEdit && !readOnly && ( - <Tooltip title={actionButtonsTooltip}> - <Stack direction='row' spacing={1} justifyContent='flex-end' alignItems='center' sx={{ mb: { xs: 2 } }}> - <Button - variant='outlined' - onClick={onEdit} - data-test='editFormButton' - disabled={isRegistryError || !canUserEditOrDelete} - > + <Stack direction='row' spacing={1} justifyContent='flex-end' alignItems='center' sx={{ mb: { xs: 2 } }}> + <Restricted action={editAction} fallback={<Button disabled>{editButtonText}</Button>}> + <Button variant='outlined' onClick={onEdit} data-test='editFormButton' disabled={isRegistryError}> {editButtonText} </Button> - {showDeleteButton && ( + </Restricted> + {showDeleteButton && deleteAction && ( + <Restricted action={deleteAction} fallback={<Button disabled>{deleteButtonText}</Button>}> <Button variant='contained' color='secondary' onClick={onDelete} data-test='deleteFormButton' - disabled={isRegistryError || !canUserEditOrDelete} + disabled={isRegistryError} > {deleteButtonText} </Button> - )} - </Stack> - </Tooltip> + </Restricted> + )} + </Stack> )} {isEdit && ( <Stack direction='row' spacing={1} justifyContent='flex-end' alignItems='center' sx={{ mb: { xs: 2 } }}> diff --git a/frontend/src/contexts/userPermissionsContext.ts b/frontend/src/contexts/userPermissionsContext.ts index 72eb585ad..ace7772c3 100644 --- a/frontend/src/contexts/userPermissionsContext.ts +++ b/frontend/src/contexts/userPermissionsContext.ts @@ -1,9 +1,10 @@ import { createContext } from 'react' -import { defaultPermissions, UserPermissionsHook } from 'src/hooks/UserPermissionsHook' +import { defaultUserPermissions, UserPermissionsHook } from 'src/hooks/UserPermissionsHook' const UserPermissionsContext = createContext<UserPermissionsHook>({ - userPermissions: defaultPermissions, + userPermissions: defaultUserPermissions, setEntryId: () => undefined, + setAccessRequestId: () => undefined, }) export default UserPermissionsContext diff --git a/frontend/src/entry/model/InferenceServices.tsx b/frontend/src/entry/model/InferenceServices.tsx index 4c62efca7..e0666e5d6 100644 --- a/frontend/src/entry/model/InferenceServices.tsx +++ b/frontend/src/entry/model/InferenceServices.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Container, Stack, Tooltip, Typography } from '@mui/material' +import { Box, Button, Container, Stack, Typography } from '@mui/material' import { sendTokenToService, useGetInferencesForModelId } from 'actions/inferencing' import { useGetUiConfig } from 'actions/uiConfig' import { deleteUserToken, postUserToken, useGetUserTokens } from 'actions/user' @@ -6,18 +6,17 @@ import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' import EmptyBlob from 'src/common/EmptyBlob' import Loading from 'src/common/Loading' +import Restricted from 'src/common/Restricted' import InferenceDisplay from 'src/entry/model/inferencing/InferenceDisplay' import MessageAlert from 'src/MessageAlert' import { EntryInterface } from 'types/types' import { getErrorMessage } from 'utils/fetcher' -import { getRequiredRolesText, hasRole } from 'utils/roles' type InferenceProps = { model: EntryInterface - currentUserRoles: string[] } -export default function InferenceServices({ model, currentUserRoles }: InferenceProps) { +export default function InferenceServices({ model }: InferenceProps) { const router = useRouter() const { inferences, isInferencesLoading, isInferencesError } = useGetInferencesForModelId(model.id) const [errorMessage, setErrorMessage] = useState('') @@ -43,11 +42,6 @@ export default function InferenceServices({ model, currentUserRoles }: Inference checkAuthentication() }, [uiConfig]) - const [canCreateService, requiredRolesText] = useMemo(() => { - const validRoles = ['owner', 'mtr', 'msro', 'contributor'] - return [hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) - const inferenceDisplays = useMemo( () => inferences.length ? ( @@ -121,13 +115,11 @@ export default function InferenceServices({ model, currentUserRoles }: Inference {healthCheck ? ( <Stack spacing={4}> <Box sx={{ textAlign: 'right' }}> - <Tooltip title={requiredRolesText}> - <span> - <Button variant='outlined' disabled={!canCreateService} onClick={handleCreateNewInferenceService}> - Create Service - </Button> - </span> - </Tooltip> + <Restricted action='createInferenceService' fallback={<Button disabled>Create Service</Button>}> + <Button variant='outlined' onClick={handleCreateNewInferenceService}> + Create Service + </Button> + </Restricted> </Box> {isInferencesLoading && <Loading />} {inferenceDisplays} diff --git a/frontend/src/entry/model/ModelImages.tsx b/frontend/src/entry/model/ModelImages.tsx index bb984350b..7f80a737a 100644 --- a/frontend/src/entry/model/ModelImages.tsx +++ b/frontend/src/entry/model/ModelImages.tsx @@ -1,22 +1,21 @@ -import { Box, Button, Container, Stack, Tooltip } from '@mui/material' +import { Box, Button, Container, Stack } from '@mui/material' import { useGetModelImages } from 'actions/model' import { useMemo, useState } from 'react' import EmptyBlob from 'src/common/EmptyBlob' import Forbidden from 'src/common/Forbidden' import Loading from 'src/common/Loading' +import Restricted from 'src/common/Restricted' import ModelImageDisplay from 'src/entry/model/registry/ModelImageDisplay' import UploadModelImageDialog from 'src/entry/model/registry/UploadModelImageDialog' import MessageAlert from 'src/MessageAlert' import { EntryInterface } from 'types/types' -import { getRequiredRolesText, hasRole } from 'utils/roles' type AccessRequestsProps = { model: EntryInterface readOnly?: boolean - currentUserRoles: string[] } -export default function ModelImages({ model, currentUserRoles, readOnly = false }: AccessRequestsProps) { +export default function ModelImages({ model, readOnly = false }: AccessRequestsProps) { const { modelImages, isModelImagesLoading, isModelImagesError } = useGetModelImages(model.id) const [openUploadImageDialog, setOpenUploadImageDialog] = useState(false) @@ -33,11 +32,6 @@ export default function ModelImages({ model, currentUserRoles, readOnly = false [modelImages, model.name], ) - const [canPushImage, requiredRolesText] = useMemo(() => { - const validRoles = ['owner', 'mtr', 'msro', 'contributor'] - return [hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) - if (isModelImagesError) { if (isModelImagesError.status === 403) { return ( @@ -59,18 +53,13 @@ export default function ModelImages({ model, currentUserRoles, readOnly = false <Stack spacing={4}> {!readOnly && ( <> - <Tooltip title={requiredRolesText}> - <Box sx={{ textAlign: 'right' }}> - <Button - variant='outlined' - disabled={!canPushImage} - onClick={() => setOpenUploadImageDialog(true)} - data-test='pushImageButton' - > + <Box sx={{ textAlign: 'right' }}> + <Restricted action='pushModelImage' fallback={<Button disabled>Push Image</Button>}> + <Button variant='outlined' onClick={() => setOpenUploadImageDialog(true)} data-test='pushImageButton'> Push Image </Button> - </Box> - </Tooltip> + </Restricted> + </Box> <UploadModelImageDialog open={openUploadImageDialog} handleClose={() => setOpenUploadImageDialog(false)} diff --git a/frontend/src/entry/model/Releases.tsx b/frontend/src/entry/model/Releases.tsx index 181a21a18..55a4ced8c 100644 --- a/frontend/src/entry/model/Releases.tsx +++ b/frontend/src/entry/model/Releases.tsx @@ -1,13 +1,14 @@ -import { Box, Button, Container, Stack, Tooltip } from '@mui/material' +import { Box, Button, Container, Stack } from '@mui/material' import { useGetReleasesForModelId } from 'actions/release' import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' import EmptyBlob from 'src/common/EmptyBlob' import Loading from 'src/common/Loading' +import Restricted from 'src/common/Restricted' import ReleaseDisplay from 'src/entry/model/releases/ReleaseDisplay' import MessageAlert from 'src/MessageAlert' import { EntryInterface } from 'types/types' -import { getRequiredRolesText, hasRole } from 'utils/roles' +import { hasRole } from 'utils/roles' type ReleasesProps = { model: EntryInterface @@ -35,11 +36,6 @@ export default function Releases({ model, currentUserRoles, readOnly = false }: [latestRelease, model, releases, currentUserRoles, readOnly], ) - const [canDraftRelease, requiredRolesText] = useMemo(() => { - const validRoles = ['owner', 'mtr', 'msro', 'contributor'] - return [hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) - useEffect(() => { if (model && releases.length > 0) { setLatestRelease(releases[0].semver) @@ -58,18 +54,18 @@ export default function Releases({ model, currentUserRoles, readOnly = false }: <Container sx={{ my: 2 }}> <Stack spacing={4}> {!readOnly && ( - <Tooltip title={requiredRolesText}> - <Box sx={{ textAlign: 'right' }}> + <Box sx={{ textAlign: 'right' }}> + <Restricted action='createRelease' fallback={<Button disabled>Draft new Release</Button>}> <Button variant='outlined' onClick={handleDraftNewRelease} - disabled={!canDraftRelease || !model.card} + disabled={!model.card} data-test='draftNewReleaseButton' > Draft new Release </Button> - </Box> - </Tooltip> + </Restricted> + </Box> )} {isReleasesLoading && <Loading />} {releases.length === 0 && <EmptyBlob text={`No releases found for model ${model.name}`} />} diff --git a/frontend/src/entry/model/accessRequests/EditableAccessRequestForm.tsx b/frontend/src/entry/model/accessRequests/EditableAccessRequestForm.tsx index 9b4061405..1efb67ac7 100644 --- a/frontend/src/entry/model/accessRequests/EditableAccessRequestForm.tsx +++ b/frontend/src/entry/model/accessRequests/EditableAccessRequestForm.tsx @@ -6,11 +6,9 @@ import { useGetAccessRequest, useGetAccessRequestsForModelId, } from 'actions/accessRequest' -import { useGetModel } from 'actions/model' import { useGetSchema } from 'actions/schema' -import { useGetCurrentUser } from 'actions/user' import { useRouter } from 'next/router' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useCallback, useContext, useEffect, useState } from 'react' import ConfirmationDialogue from 'src/common/ConfirmationDialogue' import CopyToClipboardButton from 'src/common/CopyToClipboardButton' import Loading from 'src/common/Loading' @@ -18,11 +16,9 @@ import UnsavedChangesContext from 'src/contexts/unsavedChangesContext' import EditableFormHeading from 'src/Form/EditableFormHeading' import JsonSchemaForm from 'src/Form/JsonSchemaForm' import MessageAlert from 'src/MessageAlert' -import { AccessRequestInterface, EntryKind, SplitSchemaNoRender } from 'types/types' -import { entitiesIncludesCurrentUser } from 'utils/entityUtils' +import { AccessRequestInterface, SplitSchemaNoRender } from 'types/types' import { getErrorMessage } from 'utils/fetcher' import { getStepsData, getStepsFromSchema, validateForm } from 'utils/formUtils' -import { getCurrentUserRoles, hasRole } from 'utils/roles' type EditableAccessRequestFormProps = { accessRequest: AccessRequestInterface @@ -44,26 +40,10 @@ export default function EditableAccessRequestForm({ const { schema, isSchemaLoading, isSchemaError } = useGetSchema(accessRequest.schemaId) const { isAccessRequestError, mutateAccessRequest } = useGetAccessRequest(accessRequest.modelId, accessRequest.id) const { mutateAccessRequests } = useGetAccessRequestsForModelId(accessRequest.modelId) - const { model, isModelLoading, isModelError } = useGetModel(accessRequest.modelId, EntryKind.MODEL) - const { currentUser, isCurrentUserLoading, isCurrentUserError } = useGetCurrentUser() const { setUnsavedChanges } = useContext(UnsavedChangesContext) - const router = useRouter() - - const currentUserRoles = useMemo(() => getCurrentUserRoles(model, currentUser), [model, currentUser]) - const [canUserEditOrDelete, actionButtonsTooltip] = useMemo(() => { - const isUserOwner = hasRole(currentUserRoles, ['owner']) - const isUserNamedInAccessRequest = entitiesIncludesCurrentUser( - accessRequest.metadata.overview.entities, - currentUser, - ) - const actionButtonsTooltip = !(isUserOwner || isUserNamedInAccessRequest) - ? 'Only model owners or additional contacts can edit/delete an Access Request' - : '' - - return [isUserOwner || isUserNamedInAccessRequest, actionButtonsTooltip] - }, [accessRequest.metadata.overview.entities, currentUser, currentUserRoles]) + const router = useRouter() const handleDeleteConfirm = useCallback(async () => { setErrorMessage('') @@ -138,17 +118,9 @@ export default function EditableAccessRequestForm({ return <MessageAlert message={isAccessRequestError.info.message} severity='error' /> } - if (isModelError) { - return <MessageAlert message={isModelError.info.message} severity='error' /> - } - - if (isCurrentUserError) { - return <MessageAlert message={isCurrentUserError.info.message} severity='error' /> - } - return ( <> - {(isSchemaLoading || isModelLoading || isCurrentUserLoading) && <Loading />} + {isSchemaLoading && <Loading />} <Box sx={{ py: 1 }}> <EditableFormHeading heading={ @@ -164,10 +136,10 @@ export default function EditableAccessRequestForm({ </Stack> </div> } + editAction='editAccessRequest' + deleteAction='deleteAccessRequest' editButtonText='Edit Access Request' deleteButtonText='Delete Request' - canUserEditOrDelete={canUserEditOrDelete} - actionButtonsTooltip={actionButtonsTooltip} isEdit={isEdit} isLoading={isLoading} onEdit={handleEdit} diff --git a/frontend/src/entry/model/inferencing/EditableInference.tsx b/frontend/src/entry/model/inferencing/EditableInference.tsx index f7788764e..6f01a58c1 100644 --- a/frontend/src/entry/model/inferencing/EditableInference.tsx +++ b/frontend/src/entry/model/inferencing/EditableInference.tsx @@ -1,7 +1,7 @@ import { Stack, Typography } from '@mui/material' import { putInference, UpdateInferenceParams, useGetInference } from 'actions/inferencing' import { useGetModel } from 'actions/model' -import { useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { useCallback, useContext, useEffect, useState } from 'react' import Loading from 'src/common/Loading' import UnsavedChangesContext from 'src/contexts/unsavedChangesContext' import InferenceForm from 'src/entry/model/inferencing/InferenceForm' @@ -10,15 +10,13 @@ import MessageAlert from 'src/MessageAlert' import { EntryKind, InferenceInterface } from 'types/types' import { FlattenedModelImage } from 'types/types' import { getErrorMessage } from 'utils/fetcher' -import { getRequiredRolesText, hasRole } from 'utils/roles' import { isValidPortNumber } from 'utils/stringUtils' type EditableInferenceProps = { inference: InferenceInterface - currentUserRoles: string[] } -export default function EditableInference({ inference, currentUserRoles }: EditableInferenceProps) { +export default function EditableInference({ inference }: EditableInferenceProps) { const [image, setImage] = useState<FlattenedModelImage>({ name: inference.image, tag: inference.tag, @@ -37,11 +35,6 @@ export default function EditableInference({ inference, currentUserRoles }: Edita const { mutateInference } = useGetInference(inference.modelId, inference.image, inference.tag) const { setUnsavedChanges } = useContext(UnsavedChangesContext) - const [canUserEditOrDelete, actionButtonsTooltip] = useMemo(() => { - const validRoles = ['owner', 'mtr', 'msro', 'contributor'] - return [hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) - const resetForm = useCallback(() => { setDescription(inference.description) setPort(inference.settings.port.toString()) @@ -117,8 +110,7 @@ export default function EditableInference({ inference, currentUserRoles }: Edita onEdit={handleEdit} onCancel={handleCancel} onSubmit={handleSubmit} - canUserEditOrDelete={canUserEditOrDelete} - actionButtonsTooltip={actionButtonsTooltip} + editAction='editInferenceService' errorMessage={errorMessage} isRegistryError={isRegistryError} editButtonText='Edit Settings' diff --git a/frontend/src/entry/model/mirroredModels/ExportModelAgreement.tsx b/frontend/src/entry/model/mirroredModels/ExportModelAgreement.tsx index d5277012a..70078cb87 100644 --- a/frontend/src/entry/model/mirroredModels/ExportModelAgreement.tsx +++ b/frontend/src/entry/model/mirroredModels/ExportModelAgreement.tsx @@ -1,7 +1,8 @@ import { LoadingButton } from '@mui/lab' -import { Box, Card, Checkbox, Container, FormControlLabel, Stack, Typography } from '@mui/material' +import { Box, Button, Card, Checkbox, Container, FormControlLabel, Stack, Typography } from '@mui/material' import { postModelExportToS3 } from 'actions/model' import { ChangeEvent, FormEvent, useState } from 'react' +import Restricted from 'src/common/Restricted' import ModelExportAgreementText from 'src/entry/model/mirroredModels/ModelExportAgreementText' import useNotification from 'src/hooks/useNotification' import MessageAlert from 'src/MessageAlert' @@ -57,9 +58,11 @@ export default function ExportModelAgreement({ modelId }: ExportModelAgreementPr control={<Checkbox checked={checked} onChange={handleChecked} />} label='I agree to the terms and conditions of this model export agreement' /> - <LoadingButton variant='contained' loading={loading} disabled={!checked} type='submit'> - Submit - </LoadingButton> + <Restricted action='exportMirroredModel' fallback={<Button disabled>Submit</Button>}> + <LoadingButton variant='contained' loading={loading} disabled={!checked} type='submit'> + Submit + </LoadingButton> + </Restricted> <MessageAlert message={errorMessage} severity='error' /> </Stack> </Box> diff --git a/frontend/src/entry/model/releases/EditableRelease.tsx b/frontend/src/entry/model/releases/EditableRelease.tsx index 4893dc157..78204d44b 100644 --- a/frontend/src/entry/model/releases/EditableRelease.tsx +++ b/frontend/src/entry/model/releases/EditableRelease.tsx @@ -29,24 +29,16 @@ import { SuccessfulFileUpload, } from 'types/types' import { getErrorMessage } from 'utils/fetcher' -import { getRequiredRolesText, hasRole } from 'utils/roles' import { plural } from 'utils/stringUtils' type EditableReleaseProps = { release: ReleaseInterface - currentUserRoles: string[] isEdit: boolean onIsEditChange: (value: boolean) => void readOnly?: boolean } -export default function EditableRelease({ - release, - currentUserRoles, - isEdit, - onIsEditChange, - readOnly = false, -}: EditableReleaseProps) { +export default function EditableRelease({ release, isEdit, onIsEditChange, readOnly = false }: EditableReleaseProps) { const [semver, setSemver] = useState(release.semver) const [releaseNotes, setReleaseNotes] = useState(release.notes) const [isMinorRelease, setIsMinorRelease] = useState(!!release.minor) @@ -71,11 +63,6 @@ export default function EditableRelease({ const { setUnsavedChanges } = useContext(UnsavedChangesContext) const router = useRouter() - const [canUserEditOrDelete, actionButtonsTooltip] = useMemo(() => { - const validRoles = ['owner', 'mtr', 'msro', 'contributor'] - return [hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) - const handleRegistryError = useCallback((value: boolean) => setIsRegistryError(value), []) const handleDeleteConfirm = useCallback(async () => { @@ -241,11 +228,11 @@ export default function EditableRelease({ <Typography>{`${model.name} - ${release.semver}`}</Typography> </div> } + editAction='editRelease' + deleteAction='deleteRelease' editButtonText='Edit Release' deleteButtonText='Delete Release' showDeleteButton - canUserEditOrDelete={canUserEditOrDelete} - actionButtonsTooltip={actionButtonsTooltip} isEdit={isEdit} isLoading={isLoading} onEdit={handleEdit} diff --git a/frontend/src/entry/settings/Settings.tsx b/frontend/src/entry/settings/Settings.tsx index 087a4a8cc..aee2115b4 100644 --- a/frontend/src/entry/settings/Settings.tsx +++ b/frontend/src/entry/settings/Settings.tsx @@ -12,7 +12,6 @@ import EntryAccessTab from 'src/entry/settings/EntryAccessTab' import EntryDetails from 'src/entry/settings/EntryDetails' import MessageAlert from 'src/MessageAlert' import { EntryInterface, EntryKind, UiConfig } from 'types/types' -import { hasRole } from 'utils/roles' import { toTitleCase } from 'utils/stringUtils' export const SettingsCategory = { @@ -52,10 +51,9 @@ function isSettingsCategory( type SettingsProps = { entry: EntryInterface - currentUserRoles: string[] } -export default function Settings({ entry, currentUserRoles }: SettingsProps) { +export default function Settings({ entry }: SettingsProps) { const router = useRouter() const { category } = router.query @@ -64,16 +62,6 @@ export default function Settings({ entry, currentUserRoles }: SettingsProps) { const [selectedCategory, setSelectedCategory] = useState<SettingsCategoryKeys>(SettingsCategory.DETAILS) - useEffect(() => { - const validRoles = ['owner'] - if (!hasRole(currentUserRoles, validRoles)) { - const { category: _category, ...filteredQuery } = router.query - router.replace({ - query: { ...filteredQuery, tab: 'overview' }, - }) - } - }, [currentUserRoles, router]) - useEffect(() => { if (isSettingsCategory(category, entry, uiConfig)) { setSelectedCategory(category) diff --git a/frontend/src/hooks/UserPermissionsHook.ts b/frontend/src/hooks/UserPermissionsHook.ts index 83eba0527..28e4ee054 100644 --- a/frontend/src/hooks/UserPermissionsHook.ts +++ b/frontend/src/hooks/UserPermissionsHook.ts @@ -1,49 +1,57 @@ +import { useGetCurrentUserPermissionsForAccessRequest } from 'actions/accessRequest' import { useGetCurrentUserPermissionsForEntry } from 'actions/model' -import { useEffect, useState } from 'react' -import { PermissionDetail, UserPermissions } from 'types/types' +import { useMemo, useState } from 'react' +import { AccessRequestUserPermissions, EntryUserPermissions, PermissionDetail, UserPermissions } from 'types/types' export type UserPermissionsHook = { userPermissions: UserPermissions - setEntryId: (entryId: string) => void + setEntryId: (entryId?: string) => void + setAccessRequestId: (accessRequestId?: string) => void } const defaultPermissionDetail: PermissionDetail = { hasPermission: false, info: 'Permission not set.', } -export const defaultPermissions: UserPermissions = { - editEntry: defaultPermissionDetail, - editEntryCard: defaultPermissionDetail, + +const defaultAccessRequestPermissions: AccessRequestUserPermissions = { editAccessRequest: defaultPermissionDetail, deleteAccessRequest: defaultPermissionDetail, - reviewAccessRequest: defaultPermissionDetail, +} + +const defaultEntryPermissions: EntryUserPermissions = { + editEntry: defaultPermissionDetail, + editEntryCard: defaultPermissionDetail, createRelease: defaultPermissionDetail, editRelease: defaultPermissionDetail, deleteRelease: defaultPermissionDetail, - reviewRelease: defaultPermissionDetail, pushModelImage: defaultPermissionDetail, createInferenceService: defaultPermissionDetail, editInferenceService: defaultPermissionDetail, exportMirroredModel: defaultPermissionDetail, } +export const defaultUserPermissions = { ...defaultEntryPermissions, ...defaultAccessRequestPermissions } + export default function useUserPermissions(): UserPermissionsHook { - const [userPermissions, setUserPermissions] = useState<UserPermissions>(defaultPermissions) - const [entryId, setEntryId] = useState('') - - const { - userPermissions: permissions, - isUserPermissionsLoading, - isUserPermissionsError, - } = useGetCurrentUserPermissionsForEntry(entryId) - - useEffect(() => { - if (permissions && !isUserPermissionsLoading && !isUserPermissionsError) { - setUserPermissions(permissions) - } - }, [isUserPermissionsError, isUserPermissionsLoading, permissions]) + const [entryId, setEntryId] = useState<string | undefined>(undefined) + const [accessRequestId, setAccessRequestId] = useState<string | undefined>(undefined) + + const { accessRequestUserPermissions } = useGetCurrentUserPermissionsForAccessRequest(entryId, accessRequestId) + + const { entryUserPermissions } = useGetCurrentUserPermissionsForEntry(entryId) + + const userPermissions = useMemo( + () => ({ + ...(entryUserPermissions ? entryUserPermissions : defaultEntryPermissions), + ...(accessRequestUserPermissions ? accessRequestUserPermissions : defaultAccessRequestPermissions), + }), + [accessRequestUserPermissions, entryUserPermissions], + ) + return { userPermissions, setEntryId, + setAccessRequestId, } } diff --git a/frontend/src/hooks/useGetPermissions.ts b/frontend/src/hooks/useGetPermissions.ts new file mode 100644 index 000000000..a5ea1ac3d --- /dev/null +++ b/frontend/src/hooks/useGetPermissions.ts @@ -0,0 +1,19 @@ +import { useContext, useEffect } from 'react' +import UserPermissionsContext from 'src/contexts/userPermissionsContext' + +export const useGetPermissions = (entryId?: string, accessRequestId?: string) => { + const { setAccessRequestId, setEntryId } = useContext(UserPermissionsContext) + + useEffect(() => { + if (accessRequestId) { + setAccessRequestId(accessRequestId) + } + if (entryId) { + setEntryId(entryId) + } + return () => { + setAccessRequestId(undefined) + setEntryId(undefined) + } + }, [accessRequestId, entryId, setAccessRequestId, setEntryId]) +} diff --git a/frontend/types/types.ts b/frontend/types/types.ts index f800c1ff5..3da647ad7 100644 --- a/frontend/types/types.ts +++ b/frontend/types/types.ts @@ -560,20 +560,23 @@ export type PermissionDetail = info: string } -export interface UserPermissions { +export type EntryUserPermissions = { editEntry: PermissionDetail editEntryCard: PermissionDetail - editAccessRequest: PermissionDetail - deleteAccessRequest: PermissionDetail - reviewAccessRequest: PermissionDetail createRelease: PermissionDetail editRelease: PermissionDetail deleteRelease: PermissionDetail - reviewRelease: PermissionDetail pushModelImage: PermissionDetail createInferenceService: PermissionDetail editInferenceService: PermissionDetail exportMirroredModel: PermissionDetail } +export type AccessRequestUserPermissions = { + editAccessRequest: PermissionDetail + deleteAccessRequest: PermissionDetail +} + +export type UserPermissions = EntryUserPermissions & AccessRequestUserPermissions + export type RestrictedActionKeys = keyof UserPermissions diff --git a/frontend/utils/roles.ts b/frontend/utils/roles.ts index 7bf829426..d2803f21e 100644 --- a/frontend/utils/roles.ts +++ b/frontend/utils/roles.ts @@ -1,5 +1,4 @@ import { EntryInterface, Role, User } from 'types/types' -import { toTitleCase } from 'utils/stringUtils' export function getRoleDisplay(roleId: string, modelRoles: Role[]) { const role = modelRoles.find((role) => role.id === roleId) @@ -15,14 +14,3 @@ export const hasRole = (userRoles: string[], validRoles: string[]) => { export const getCurrentUserRoles = (entry: EntryInterface | undefined, currentUser: User | undefined) => { return entry?.collaborators.find((collaborator) => collaborator.entity.split(':')[1] === currentUser?.dn)?.roles || [] } - -export const getRequiredRolesText = (userRoles: string[], validRoles: string[]) => { - if (!validRoles.length || hasRole(userRoles, validRoles)) { - return '' - } else if (validRoles.length === 1) { - return `${toTitleCase(validRoles[0])} role required` - } - - const last = toTitleCase(`${validRoles.pop()}`) - return `${validRoles.map((role) => toTitleCase(role)).join(', ')} or ${last} role required` -} From 69a781e7aabadd16631abc99885fc56671827b39 Mon Sep 17 00:00:00 2001 From: araddcc002 <araddcc002@euro.ngc.com> Date: Wed, 30 Oct 2024 17:26:25 +0000 Subject: [PATCH 04/11] fixed unit test for EditableFormHeading --- frontend/test/Form/EditableFormHeading.spec.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/test/Form/EditableFormHeading.spec.tsx b/frontend/test/Form/EditableFormHeading.spec.tsx index 5b42836bb..bdea24db9 100644 --- a/frontend/test/Form/EditableFormHeading.spec.tsx +++ b/frontend/test/Form/EditableFormHeading.spec.tsx @@ -10,6 +10,7 @@ describe('EditableFormHeading', () => { isEdit={false} heading='Test' isLoading={false} + editAction='editRelease' editButtonText='Edit Form' onEdit={() => undefined} onCancel={() => undefined} @@ -30,6 +31,7 @@ describe('EditableFormHeading', () => { isEdit={true} heading='Test' isLoading={false} + editAction='editRelease' editButtonText='Edit Form' onEdit={() => undefined} onCancel={() => undefined} From d5c01dd4e3b1958010d954aba7ca124687812403 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:21:46 +0000 Subject: [PATCH 05/11] Simplified how entryId and accessRequestId are passed to permissions hook --- frontend/actions/accessRequest.ts | 40 +++++++++---------- frontend/actions/model.ts | 34 ++++++++-------- frontend/pages/_app.tsx | 6 ++- frontend/pages/data-card/[dataCardId].tsx | 2 - frontend/pages/model/[modelId].tsx | 2 - .../access-request/[accessRequestId].tsx | 3 -- .../model/[modelId]/release/[semver].tsx | 3 -- .../src/contexts/userPermissionsContext.ts | 2 - frontend/src/hooks/UserPermissionsHook.ts | 14 ++----- frontend/src/hooks/useGetPermissions.ts | 19 --------- 10 files changed, 45 insertions(+), 80 deletions(-) delete mode 100644 frontend/src/hooks/useGetPermissions.ts diff --git a/frontend/actions/accessRequest.ts b/frontend/actions/accessRequest.ts index 8d7e03720..774212d35 100644 --- a/frontend/actions/accessRequest.ts +++ b/frontend/actions/accessRequest.ts @@ -30,12 +30,31 @@ export function useGetAccessRequest(modelId: string | undefined, accessRequestId return { mutateAccessRequest: mutate, - accessRequest: data ? data.accessRequest : undefined, + accessRequest: data?.accessRequest, isAccessRequestLoading: isLoading, isAccessRequestError: error, } } +export function useGetCurrentUserPermissionsForAccessRequest(entryId?: string, accessRequestId?: string) { + const { data, isLoading, error, mutate } = useSWR< + { + permissions: AccessRequestUserPermissions + }, + ErrorInfo + >( + entryId && accessRequestId ? `/api/v2/model/${entryId}/access-request/${accessRequestId}/permissions/mine` : null, + fetcher, + ) + + return { + mutateAccessRequestUserPermissions: mutate, + accessRequestUserPermissions: data?.permissions, + isAccessRequestUserPermissionsLoading: isLoading, + isAccessRequestUserPermissionsError: error, + } +} + export function postAccessRequest(modelId: string, schemaId: string, form: Record<string, unknown>) { return fetch(`/api/v2/model/${modelId}/access-requests`, { method: 'post', @@ -66,22 +85,3 @@ export function postAccessRequestComment(modelId: string, accessRequestId: strin body: JSON.stringify({ comment }), }) } - -export function useGetCurrentUserPermissionsForAccessRequest(entryId?: string, accessRequestId?: string) { - const { data, isLoading, error, mutate } = useSWR< - { - permissions: AccessRequestUserPermissions - }, - ErrorInfo - >( - entryId && accessRequestId ? `/api/v2/model/${entryId}/access-request/${accessRequestId}/permissions/mine` : null, - fetcher, - ) - - return { - mutateAccessRequestUserPermissions: mutate, - accessRequestUserPermissions: data ? data.permissions : undefined, - isAccessRequestUserPermissionsLoading: isLoading, - isAccessRequestUserPermissionsError: error, - } -} diff --git a/frontend/actions/model.ts b/frontend/actions/model.ts index 140f80b84..0b7572a74 100644 --- a/frontend/actions/model.ts +++ b/frontend/actions/model.ts @@ -65,7 +65,7 @@ export function useGetModel(id: string | undefined, kind: EntryKindKeys) { return { mutateModel: mutate, - model: data ? data.model : undefined, + model: data?.model, isModelLoading: isLoading, isModelError: error, } @@ -125,6 +125,22 @@ export function useGetModelRolesCurrentUser(id?: string) { } } +export function useGetCurrentUserPermissionsForEntry(entryId?: string) { + const { data, isLoading, error, mutate } = useSWR< + { + permissions: EntryUserPermissions + }, + ErrorInfo + >(entryId ? `/api/v2/model/${entryId}/permissions/mine` : null, fetcher) + + return { + mutateEntryUserPermissions: mutate, + entryUserPermissions: data?.permissions, + isEntryUserPermissionsLoading: isLoading, + isEntryUserPermissionsError: error, + } +} + export async function postModel(form: EntryForm) { return fetch(`/api/v2/models`, { method: 'post', @@ -151,19 +167,3 @@ export async function postModelExportToS3(id: string, modelExport: ModelExportRe body: JSON.stringify(modelExport), }) } - -export function useGetCurrentUserPermissionsForEntry(entryId?: string) { - const { data, isLoading, error, mutate } = useSWR< - { - permissions: EntryUserPermissions - }, - ErrorInfo - >(entryId ? `/api/v2/model/${entryId}/permissions/mine` : null, fetcher) - - return { - mutateEntryUserPermissions: mutate, - entryUserPermissions: data ? data.permissions : undefined, - isEntryUserPermissionsLoading: isLoading, - isEntryUserPermissionsError: error, - } -} diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 09cc1e5d8..03784a3e2 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -10,6 +10,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AppProps } from 'next/app' import Head from 'next/head' +import { useRouter } from 'next/router' import { SnackbarProvider } from 'notistack' import { useEffect } from 'react' import UserPermissionsContext from 'src/contexts/userPermissionsContext' @@ -25,7 +26,10 @@ export default function MyApp(props: AppProps) { const { Component, pageProps } = props const themeModeValue = useThemeMode() const unsavedChangesValue = useUnsavedChanges() - const userPermissions = useUserPermissions() + const router = useRouter() + const { modelId, dataCardId, accessRequestId }: { modelId?: string; dataCardId?: string; accessRequestId?: string } = + router.query + const userPermissions = useUserPermissions(modelId || dataCardId, accessRequestId) // This is set so that 'react-markdown-editor' respects the theme set by MUI. useEffect(() => { diff --git a/frontend/pages/data-card/[dataCardId].tsx b/frontend/pages/data-card/[dataCardId].tsx index 9fbf419cf..f01f65572 100644 --- a/frontend/pages/data-card/[dataCardId].tsx +++ b/frontend/pages/data-card/[dataCardId].tsx @@ -8,7 +8,6 @@ import UserPermissionsContext from 'src/contexts/userPermissionsContext' import Overview from 'src/entry/overview/Overview' import Settings from 'src/entry/settings/Settings' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' -import { useGetPermissions } from 'src/hooks/useGetPermissions' import { EntryKind } from 'types/types' export default function DataCard() { @@ -20,7 +19,6 @@ export default function DataCard() { isModelError: isDataCardError, } = useGetModel(dataCardId, EntryKind.DATA_CARD) - useGetPermissions(dataCardId) const { userPermissions } = useContext(UserPermissionsContext) const settingsPermission = useMemo(() => userPermissions['editEntry'], [userPermissions]) diff --git a/frontend/pages/model/[modelId].tsx b/frontend/pages/model/[modelId].tsx index 6f3f59d62..06ee35017 100644 --- a/frontend/pages/model/[modelId].tsx +++ b/frontend/pages/model/[modelId].tsx @@ -14,7 +14,6 @@ import Releases from 'src/entry/model/Releases' import Overview from 'src/entry/overview/Overview' import Settings from 'src/entry/settings/Settings' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' -import { useGetPermissions } from 'src/hooks/useGetPermissions' import { EntryKind } from 'types/types' import { getCurrentUserRoles } from 'utils/roles' @@ -25,7 +24,6 @@ export default function Model() { const { currentUser, isCurrentUserLoading, isCurrentUserError } = useGetCurrentUser() const { uiConfig, isUiConfigLoading, isUiConfigError } = useGetUiConfig() - useGetPermissions(modelId) const { userPermissions } = useContext(UserPermissionsContext) const currentUserRoles = useMemo(() => getCurrentUserRoles(model, currentUser), [model, currentUser]) diff --git a/frontend/pages/model/[modelId]/access-request/[accessRequestId].tsx b/frontend/pages/model/[modelId]/access-request/[accessRequestId].tsx index 33d953ff7..99a209d6f 100644 --- a/frontend/pages/model/[modelId]/access-request/[accessRequestId].tsx +++ b/frontend/pages/model/[modelId]/access-request/[accessRequestId].tsx @@ -12,7 +12,6 @@ import Title from 'src/common/Title' import EditableAccessRequestForm from 'src/entry/model/accessRequests/EditableAccessRequestForm' import ReviewBanner from 'src/entry/model/reviews/ReviewBanner' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' -import { useGetPermissions } from 'src/hooks/useGetPermissions' import Link from 'src/Link' import ReviewComments from 'src/reviews/ReviewComments' import { EntryKind } from 'types/types' @@ -24,8 +23,6 @@ export default function AccessRequest() { const [isEdit, setIsEdit] = useState(false) - useGetPermissions(modelId, accessRequestId) - const { accessRequest, isAccessRequestLoading, isAccessRequestError } = useGetAccessRequest(modelId, accessRequestId) const { reviews, isReviewsLoading, isReviewsError } = useGetReviewRequestsForModel({ modelId, diff --git a/frontend/pages/model/[modelId]/release/[semver].tsx b/frontend/pages/model/[modelId]/release/[semver].tsx index cceae3940..1e135ac3d 100644 --- a/frontend/pages/model/[modelId]/release/[semver].tsx +++ b/frontend/pages/model/[modelId]/release/[semver].tsx @@ -12,7 +12,6 @@ import Title from 'src/common/Title' import EditableRelease from 'src/entry/model/releases/EditableRelease' import ReviewBanner from 'src/entry/model/reviews/ReviewBanner' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' -import { useGetPermissions } from 'src/hooks/useGetPermissions' import Link from 'src/Link' import ReviewComments from 'src/reviews/ReviewComments' import { EntryKind } from 'types/types' @@ -27,8 +26,6 @@ export default function Release() { const { release, isReleaseLoading, isReleaseError } = useGetRelease(modelId, semver) const { model, isModelLoading, isModelError } = useGetModel(modelId, EntryKind.MODEL) - useGetPermissions(modelId) - const { reviews, isReviewsLoading, isReviewsError } = useGetReviewRequestsForModel({ modelId, semver: semver || '', diff --git a/frontend/src/contexts/userPermissionsContext.ts b/frontend/src/contexts/userPermissionsContext.ts index ace7772c3..c9035a169 100644 --- a/frontend/src/contexts/userPermissionsContext.ts +++ b/frontend/src/contexts/userPermissionsContext.ts @@ -3,8 +3,6 @@ import { defaultUserPermissions, UserPermissionsHook } from 'src/hooks/UserPermi const UserPermissionsContext = createContext<UserPermissionsHook>({ userPermissions: defaultUserPermissions, - setEntryId: () => undefined, - setAccessRequestId: () => undefined, }) export default UserPermissionsContext diff --git a/frontend/src/hooks/UserPermissionsHook.ts b/frontend/src/hooks/UserPermissionsHook.ts index 28e4ee054..e23d83994 100644 --- a/frontend/src/hooks/UserPermissionsHook.ts +++ b/frontend/src/hooks/UserPermissionsHook.ts @@ -1,12 +1,10 @@ import { useGetCurrentUserPermissionsForAccessRequest } from 'actions/accessRequest' import { useGetCurrentUserPermissionsForEntry } from 'actions/model' -import { useMemo, useState } from 'react' +import { useMemo } from 'react' import { AccessRequestUserPermissions, EntryUserPermissions, PermissionDetail, UserPermissions } from 'types/types' export type UserPermissionsHook = { userPermissions: UserPermissions - setEntryId: (entryId?: string) => void - setAccessRequestId: (accessRequestId?: string) => void } const defaultPermissionDetail: PermissionDetail = { @@ -33,13 +31,9 @@ const defaultEntryPermissions: EntryUserPermissions = { export const defaultUserPermissions = { ...defaultEntryPermissions, ...defaultAccessRequestPermissions } -export default function useUserPermissions(): UserPermissionsHook { - const [entryId, setEntryId] = useState<string | undefined>(undefined) - const [accessRequestId, setAccessRequestId] = useState<string | undefined>(undefined) - - const { accessRequestUserPermissions } = useGetCurrentUserPermissionsForAccessRequest(entryId, accessRequestId) - +export default function useUserPermissions(entryId?: string, accessRequestId?: string): UserPermissionsHook { const { entryUserPermissions } = useGetCurrentUserPermissionsForEntry(entryId) + const { accessRequestUserPermissions } = useGetCurrentUserPermissionsForAccessRequest(entryId, accessRequestId) const userPermissions = useMemo( () => ({ @@ -51,7 +45,5 @@ export default function useUserPermissions(): UserPermissionsHook { return { userPermissions, - setEntryId, - setAccessRequestId, } } diff --git a/frontend/src/hooks/useGetPermissions.ts b/frontend/src/hooks/useGetPermissions.ts deleted file mode 100644 index a5ea1ac3d..000000000 --- a/frontend/src/hooks/useGetPermissions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useContext, useEffect } from 'react' -import UserPermissionsContext from 'src/contexts/userPermissionsContext' - -export const useGetPermissions = (entryId?: string, accessRequestId?: string) => { - const { setAccessRequestId, setEntryId } = useContext(UserPermissionsContext) - - useEffect(() => { - if (accessRequestId) { - setAccessRequestId(accessRequestId) - } - if (entryId) { - setEntryId(entryId) - } - return () => { - setAccessRequestId(undefined) - setEntryId(undefined) - } - }, [accessRequestId, entryId, setAccessRequestId, setEntryId]) -} From aff1730ccf575326cbe6216b4f9851143d54284f Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:49:34 +0000 Subject: [PATCH 06/11] Moved router query parsing into user permissions hook --- frontend/pages/_app.tsx | 6 +----- frontend/src/hooks/UserPermissionsHook.ts | 8 +++++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 03784a3e2..09cc1e5d8 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -10,7 +10,6 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider' import { AppProps } from 'next/app' import Head from 'next/head' -import { useRouter } from 'next/router' import { SnackbarProvider } from 'notistack' import { useEffect } from 'react' import UserPermissionsContext from 'src/contexts/userPermissionsContext' @@ -26,10 +25,7 @@ export default function MyApp(props: AppProps) { const { Component, pageProps } = props const themeModeValue = useThemeMode() const unsavedChangesValue = useUnsavedChanges() - const router = useRouter() - const { modelId, dataCardId, accessRequestId }: { modelId?: string; dataCardId?: string; accessRequestId?: string } = - router.query - const userPermissions = useUserPermissions(modelId || dataCardId, accessRequestId) + const userPermissions = useUserPermissions() // This is set so that 'react-markdown-editor' respects the theme set by MUI. useEffect(() => { diff --git a/frontend/src/hooks/UserPermissionsHook.ts b/frontend/src/hooks/UserPermissionsHook.ts index e23d83994..e2142f6bc 100644 --- a/frontend/src/hooks/UserPermissionsHook.ts +++ b/frontend/src/hooks/UserPermissionsHook.ts @@ -1,5 +1,6 @@ import { useGetCurrentUserPermissionsForAccessRequest } from 'actions/accessRequest' import { useGetCurrentUserPermissionsForEntry } from 'actions/model' +import { useRouter } from 'next/router' import { useMemo } from 'react' import { AccessRequestUserPermissions, EntryUserPermissions, PermissionDetail, UserPermissions } from 'types/types' @@ -31,7 +32,12 @@ const defaultEntryPermissions: EntryUserPermissions = { export const defaultUserPermissions = { ...defaultEntryPermissions, ...defaultAccessRequestPermissions } -export default function useUserPermissions(entryId?: string, accessRequestId?: string): UserPermissionsHook { +export default function useUserPermissions(): UserPermissionsHook { + const router = useRouter() + const { modelId, dataCardId, accessRequestId }: { modelId?: string; dataCardId?: string; accessRequestId?: string } = + router.query + const entryId = modelId || dataCardId + const { entryUserPermissions } = useGetCurrentUserPermissionsForEntry(entryId) const { accessRequestUserPermissions } = useGetCurrentUserPermissionsForAccessRequest(entryId, accessRequestId) From 2aec91babb6e06df4103d4183692e675db522142 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:02:31 +0000 Subject: [PATCH 07/11] Fixed permission tooltip positioning --- .../src/entry/model/InferenceServices.tsx | 14 ++++++----- frontend/src/entry/model/ModelImages.tsx | 18 +++++++++----- frontend/src/entry/model/Releases.tsx | 24 ++++++++++--------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/frontend/src/entry/model/InferenceServices.tsx b/frontend/src/entry/model/InferenceServices.tsx index e0666e5d6..649d734c0 100644 --- a/frontend/src/entry/model/InferenceServices.tsx +++ b/frontend/src/entry/model/InferenceServices.tsx @@ -114,12 +114,14 @@ export default function InferenceServices({ model }: InferenceProps) { <Container sx={{ my: 2 }}> {healthCheck ? ( <Stack spacing={4}> - <Box sx={{ textAlign: 'right' }}> - <Restricted action='createInferenceService' fallback={<Button disabled>Create Service</Button>}> - <Button variant='outlined' onClick={handleCreateNewInferenceService}> - Create Service - </Button> - </Restricted> + <Box display='flex'> + <Box ml='auto'> + <Restricted action='createInferenceService' fallback={<Button disabled>Create Service</Button>}> + <Button variant='outlined' onClick={handleCreateNewInferenceService}> + Create Service + </Button> + </Restricted> + </Box> </Box> {isInferencesLoading && <Loading />} {inferenceDisplays} diff --git a/frontend/src/entry/model/ModelImages.tsx b/frontend/src/entry/model/ModelImages.tsx index 7f80a737a..d37ae2e69 100644 --- a/frontend/src/entry/model/ModelImages.tsx +++ b/frontend/src/entry/model/ModelImages.tsx @@ -53,12 +53,18 @@ export default function ModelImages({ model, readOnly = false }: AccessRequestsP <Stack spacing={4}> {!readOnly && ( <> - <Box sx={{ textAlign: 'right' }}> - <Restricted action='pushModelImage' fallback={<Button disabled>Push Image</Button>}> - <Button variant='outlined' onClick={() => setOpenUploadImageDialog(true)} data-test='pushImageButton'> - Push Image - </Button> - </Restricted> + <Box display='flex'> + <Box ml='auto'> + <Restricted action='pushModelImage' fallback={<Button disabled>Push Image</Button>}> + <Button + variant='outlined' + onClick={() => setOpenUploadImageDialog(true)} + data-test='pushImageButton' + > + Push Image + </Button> + </Restricted> + </Box> </Box> <UploadModelImageDialog open={openUploadImageDialog} diff --git a/frontend/src/entry/model/Releases.tsx b/frontend/src/entry/model/Releases.tsx index 55a4ced8c..373a6094e 100644 --- a/frontend/src/entry/model/Releases.tsx +++ b/frontend/src/entry/model/Releases.tsx @@ -54,17 +54,19 @@ export default function Releases({ model, currentUserRoles, readOnly = false }: <Container sx={{ my: 2 }}> <Stack spacing={4}> {!readOnly && ( - <Box sx={{ textAlign: 'right' }}> - <Restricted action='createRelease' fallback={<Button disabled>Draft new Release</Button>}> - <Button - variant='outlined' - onClick={handleDraftNewRelease} - disabled={!model.card} - data-test='draftNewReleaseButton' - > - Draft new Release - </Button> - </Restricted> + <Box display='flex'> + <Box ml='auto'> + <Restricted action='createRelease' fallback={<Button disabled>Draft new Release</Button>}> + <Button + variant='outlined' + onClick={handleDraftNewRelease} + disabled={!model.card} + data-test='draftNewReleaseButton' + > + Draft new Release + </Button> + </Restricted> + </Box> </Box> )} {isReleasesLoading && <Loading />} From 2af12f11f1b63ae8dc2f4bd3f81478ddc3ce789b Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:04:29 +0000 Subject: [PATCH 08/11] Updated context value name to be consistent with others --- frontend/pages/_app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 09cc1e5d8..3bb424e23 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -25,7 +25,7 @@ export default function MyApp(props: AppProps) { const { Component, pageProps } = props const themeModeValue = useThemeMode() const unsavedChangesValue = useUnsavedChanges() - const userPermissions = useUserPermissions() + const userPermissionsValue = useUserPermissions() // This is set so that 'react-markdown-editor' respects the theme set by MUI. useEffect(() => { @@ -42,7 +42,7 @@ export default function MyApp(props: AppProps) { <ThemeProvider theme={themeModeValue.theme}> <UnsavedChangesContext.Provider value={unsavedChangesValue}> <ThemeModeContext.Provider value={themeModeValue}> - <UserPermissionsContext.Provider value={userPermissions}> + <UserPermissionsContext.Provider value={userPermissionsValue}> <SnackbarProvider> <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale='en-gb'> <CssBaseline /> From ed6dadf03d728ef497a40776be912f010a3d1120 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:11:11 +0000 Subject: [PATCH 09/11] Removed redundant prop --- frontend/src/Form/EditableFormHeading.tsx | 4 +--- .../entry/model/accessRequests/EditableAccessRequestForm.tsx | 1 - frontend/src/entry/model/releases/EditableRelease.tsx | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/Form/EditableFormHeading.tsx b/frontend/src/Form/EditableFormHeading.tsx index 1418a7332..8d1a39e10 100644 --- a/frontend/src/Form/EditableFormHeading.tsx +++ b/frontend/src/Form/EditableFormHeading.tsx @@ -19,7 +19,6 @@ type EditableFormHeadingProps = { deleteAction?: RestrictedActionKeys errorMessage?: string deleteButtonText?: string - showDeleteButton?: boolean readOnly?: boolean disableSaveButton?: boolean } @@ -37,7 +36,6 @@ export default function EditableFormHeading({ deleteAction, errorMessage = '', deleteButtonText = 'Delete', - showDeleteButton = false, isRegistryError = false, readOnly = false, disableSaveButton = false, @@ -58,7 +56,7 @@ export default function EditableFormHeading({ {editButtonText} </Button> </Restricted> - {showDeleteButton && deleteAction && ( + {deleteAction && deleteButtonText && ( <Restricted action={deleteAction} fallback={<Button disabled>{deleteButtonText}</Button>}> <Button variant='contained' diff --git a/frontend/src/entry/model/accessRequests/EditableAccessRequestForm.tsx b/frontend/src/entry/model/accessRequests/EditableAccessRequestForm.tsx index 1efb67ac7..cf7c07fd5 100644 --- a/frontend/src/entry/model/accessRequests/EditableAccessRequestForm.tsx +++ b/frontend/src/entry/model/accessRequests/EditableAccessRequestForm.tsx @@ -147,7 +147,6 @@ export default function EditableAccessRequestForm({ onSubmit={handleSubmit} onDelete={() => setOpen(true)} errorMessage={errorMessage} - showDeleteButton /> <JsonSchemaForm splitSchema={splitSchema} setSplitSchema={setSplitSchema} canEdit={isEdit} /> <ConfirmationDialogue diff --git a/frontend/src/entry/model/releases/EditableRelease.tsx b/frontend/src/entry/model/releases/EditableRelease.tsx index 78204d44b..467737bc8 100644 --- a/frontend/src/entry/model/releases/EditableRelease.tsx +++ b/frontend/src/entry/model/releases/EditableRelease.tsx @@ -232,7 +232,6 @@ export default function EditableRelease({ release, isEdit, onIsEditChange, readO deleteAction='deleteRelease' editButtonText='Edit Release' deleteButtonText='Delete Release' - showDeleteButton isEdit={isEdit} isLoading={isLoading} onEdit={handleEdit} From 8ed43ed18381461a20a3a798db781517d5da5708 Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:30:42 +0000 Subject: [PATCH 10/11] Changed tooltip for clarity --- frontend/src/hooks/UserPermissionsHook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hooks/UserPermissionsHook.ts b/frontend/src/hooks/UserPermissionsHook.ts index e2142f6bc..ea6259c63 100644 --- a/frontend/src/hooks/UserPermissionsHook.ts +++ b/frontend/src/hooks/UserPermissionsHook.ts @@ -10,7 +10,7 @@ export type UserPermissionsHook = { const defaultPermissionDetail: PermissionDetail = { hasPermission: false, - info: 'Permission not set.', + info: 'Fetching permissions...', } const defaultAccessRequestPermissions: AccessRequestUserPermissions = { From 2057a18a6f8539ee4f3c684315082f845efdd9bd Mon Sep 17 00:00:00 2001 From: ARADDCC012 <110473008+ARADDCC012@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:42:27 +0000 Subject: [PATCH 11/11] Reintroduced readOnly logic to FormEditPage and entry overview --- frontend/src/entry/overview/FormEditPage.tsx | 27 +++++++++++--------- frontend/src/entry/overview/Overview.tsx | 2 +- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/frontend/src/entry/overview/FormEditPage.tsx b/frontend/src/entry/overview/FormEditPage.tsx index aa46130af..a457cb089 100644 --- a/frontend/src/entry/overview/FormEditPage.tsx +++ b/frontend/src/entry/overview/FormEditPage.tsx @@ -24,8 +24,9 @@ import { EntryCardKindLabel, EntryInterface, SplitSchemaNoRender } from 'types/t import { getStepsData, getStepsFromSchema } from 'utils/formUtils' type FormEditPageProps = { entry: EntryInterface + readOnly?: boolean } -export default function FormEditPage({ entry }: FormEditPageProps) { +export default function FormEditPage({ entry, readOnly = false }: FormEditPageProps) { const [isEdit, setIsEdit] = useState(false) const [splitSchema, setSplitSchema] = useState<SplitSchemaNoRender>({ reference: '', steps: [] }) const [errorMessage, setErrorMessage] = useState('') @@ -182,17 +183,19 @@ export default function FormEditPage({ entry }: FormEditPageProps) { </ListItemIcon> <ListItemText>View History</ListItemText> </MenuItem> - <Restricted action='editEntryCard' fallback={<MenuItem disabled>{editMenuItemContent}</MenuItem>}> - <MenuItem - onClick={() => { - handleActionButtonClose() - setIsEdit(!isEdit) - }} - data-test='editEntryCardButton' - > - {editMenuItemContent} - </MenuItem> - </Restricted> + {!readOnly && ( + <Restricted action='editEntryCard' fallback={<MenuItem disabled>{editMenuItemContent}</MenuItem>}> + <MenuItem + onClick={() => { + handleActionButtonClose() + setIsEdit(!isEdit) + }} + data-test='editEntryCardButton' + > + {editMenuItemContent} + </MenuItem> + </Restricted> + )} </Menu> </> )} diff --git a/frontend/src/entry/overview/Overview.tsx b/frontend/src/entry/overview/Overview.tsx index 43b007b54..361276f8b 100644 --- a/frontend/src/entry/overview/Overview.tsx +++ b/frontend/src/entry/overview/Overview.tsx @@ -31,7 +31,7 @@ export default function Overview({ entry, readOnly = false }: OverviewProps) { ) : ( <Container sx={{ my: 2 }}> {page === OverviewPage.TEMPLATE && <TemplatePage entry={entry} />} - {page === OverviewPage.FORM && <FormEditPage entry={entry} />} + {page === OverviewPage.FORM && <FormEditPage entry={entry} readOnly={readOnly} />} </Container> ) }