feat(scaffolder): update scaffolder frontend to support additional backend permissions

Signed-off-by: Frank Kong <frkong@redhat.com>
This commit is contained in:
Frank Kong
2024-04-25 16:42:23 -04:00
parent a7b500bd08
commit b7fcaca26b
12 changed files with 309 additions and 74 deletions
@@ -35,6 +35,7 @@ import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { SWRConfig } from 'swr';
const mockAuthorize = jest.fn();
@@ -546,7 +547,7 @@ describe('<AboutCard />', () => {
).not.toBeInTheDocument();
});
it('renders techdocs lin when 3rdparty', async () => {
it('renders techdocs link when 3rdparty', async () => {
const entity = {
apiVersion: 'v1',
kind: 'Component',
@@ -774,7 +775,9 @@ describe('<AboutCard />', () => {
namespace: 'default',
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.ALLOW,
}));
await renderInTestApp(
<TestApiProvider
apis={[
@@ -794,7 +797,7 @@ describe('<AboutCard />', () => {
),
],
[catalogApiRef, catalogApi],
[permissionApiRef, {}],
[permissionApiRef, mockPermissionApi],
]}
>
<EntityProvider entity={entity}>
@@ -816,6 +819,58 @@ describe('<AboutCard />', () => {
'/create/templates/default/create-react-app-template',
);
});
it('renders disabled launch template button if user has insufficient permissions', async () => {
const entity = {
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 'create-react-app-template',
namespace: 'default',
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.DENY,
}));
const rendered = await renderInTestApp(
<SWRConfig value={{ provider: () => new Map() }}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</TestApiProvider>
</SWRConfig>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
'/create/templates/:namespace/:templateName':
createFromTemplateRouteRef,
},
},
);
expect(screen.getByText('Launch Template')).toBeVisible();
expect(screen.getByText('Launch Template').closest('a')).toBeNull();
});
it.each([
{
@@ -19,6 +19,7 @@ import {
CompoundEntityRef,
DEFAULT_NAMESPACE,
stringifyEntityRef,
parseEntityRef,
} from '@backstage/catalog-model';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
@@ -58,10 +59,11 @@ import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import DocsIcon from '@material-ui/icons/Description';
import EditIcon from '@material-ui/icons/Edit';
import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { parseEntityRef } from '@backstage/catalog-model';
import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha';
import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha';
import { useSourceTemplateCompoundEntityRef } from './hooks';
import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha';
import { usePermission } from '@backstage/plugin-permission-react';
const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
@@ -114,6 +116,11 @@ export function AboutCard(props: AboutCardProps) {
const { allowed: canRefresh } = useEntityPermission(
catalogEntityRefreshPermission,
);
const { kind, name, namespace } = entity.metadata;
const { allowed: canCreateTemplateTask } = usePermission({
permission: taskCreatePermission,
resourceRef: `${kind}:${namespace}/${name}`,
});
const entitySourceLocation = getEntitySourceLocation(
entity,
@@ -172,7 +179,7 @@ export function AboutCard(props: AboutCardProps) {
const launchTemplate: IconLinkVerticalProps = {
label: 'Launch Template',
icon: <Icon />,
disabled: !templateRoute,
disabled: !templateRoute || !canCreateTemplateTask,
href:
templateRoute &&
templateRoute({
@@ -65,6 +65,7 @@ import { validate } from 'jsonschema';
import { Logger } from 'winston';
import { z } from 'zod';
import {
TemplateAction,
TaskBroker,
TemplateFilter,
TemplateGlobal,
@@ -78,7 +79,6 @@ import {
import { createDryRunner } from '../scaffolder/dryrun';
import { StorageTaskBroker } from '../scaffolder/tasks/StorageTaskBroker';
import { findTemplate, getEntityBaseUrl, getWorkingDirectory } from './helpers';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import {
AuthorizeResult,
PermissionRuleParams,
@@ -491,7 +491,7 @@ export async function createRouter(
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
await permissions.authorizeConditional(
[{ permission: actionReadPermission }],
{ credentials: credentials },
)
@@ -520,7 +520,7 @@ export async function createRouter(
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
await permissions.authorizeConditional(
[{ permission: taskCreatePermission }],
{ credentials: credentials },
)
@@ -611,7 +611,7 @@ export async function createRouter(
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
await permissions.authorizeConditional(
[{ permission: taskReadPermission }],
{ credentials: credentials },
)
@@ -648,7 +648,7 @@ export async function createRouter(
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
await permissions.authorizeConditional(
[{ permission: taskReadPermission }],
{ credentials: credentials },
)
@@ -672,7 +672,7 @@ export async function createRouter(
const credentials = await httpAuth.credentials(req);
if (permissions) {
const authorizationResponses = await permissions?.authorizeConditional(
const authorizationResponses = await permissions.authorizeConditional(
[
{ permission: taskCancelPermission },
{ permission: taskReadPermission },
@@ -696,7 +696,7 @@ export async function createRouter(
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
await permissions.authorizeConditional(
[{ permission: taskReadPermission }],
{ credentials: credentials },
)
@@ -758,7 +758,7 @@ export async function createRouter(
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
await permissions.authorizeConditional(
[{ permission: taskReadPermission }],
{ credentials: credentials },
)
@@ -802,7 +802,7 @@ export async function createRouter(
if (permissions) {
const authorizationResponse = (
await permissions?.authorizeConditional(
await permissions.authorizeConditional(
[{ permission: taskCreatePermission }],
{ credentials: credentials },
)
+4 -1
View File
@@ -54,6 +54,7 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
@@ -87,13 +88,15 @@
"@backstage/core-app-api": "workspace:^",
"@backstage/plugin-catalog": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/humanize-duration": "^3.18.1",
"@types/luxon": "^3.0.0"
"@types/luxon": "^3.0.0",
"swr": "^2.0.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
@@ -19,6 +19,7 @@ import {
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import {
MockPermissionApi,
MockStorageApi,
renderInTestApp,
TestApiProvider,
@@ -28,6 +29,12 @@ import React from 'react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { fireEvent } from '@testing-library/react';
import {
PermissionApi,
permissionApiRef,
} from '@backstage/plugin-permission-react';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { SWRConfig } from 'swr';
describe('TemplateCard', () => {
it('should render the card title', async () => {
@@ -50,6 +57,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -79,6 +87,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -110,6 +119,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -139,6 +149,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -174,6 +185,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -213,6 +225,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -257,6 +270,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} additionalLinks={[]} />
@@ -305,6 +319,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} additionalLinks={[]} />
@@ -347,6 +362,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -386,6 +402,7 @@ describe('TemplateCard', () => {
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi()],
]}
>
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
@@ -403,4 +420,44 @@ describe('TemplateCard', () => {
expect(mockOnSelected).toHaveBeenCalledWith(mockTemplate);
});
it('should not render the choose button when user has insufficient permissions', async () => {
const mockTemplate: TemplateEntityV1beta3 = {
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: { name: 'bob', tags: ['cpp', 'react'] },
spec: {
steps: [],
type: 'service',
},
};
const mockOnSelected = jest.fn();
const mockAuthorize = jest
.fn()
.mockImplementation(async () => ({ result: AuthorizeResult.DENY }));
// SWR used by the usePermission hook needs cache to be reset for each test
const { queryByText } = await renderInTestApp(
<SWRConfig value={{ provider: () => new Map() }}>
<TestApiProvider
apis={[
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
}),
],
[permissionApiRef, new MockPermissionApi(mockAuthorize)],
]}
>
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
</TestApiProvider>
</SWRConfig>,
{
mountedRoutes: {
'/catalog/:kind/:namespace/:name': entityRouteRef,
},
},
);
expect(queryByText('Choose')).toBeNull();
});
});
@@ -35,6 +35,8 @@ import LanguageIcon from '@material-ui/icons/Language';
import React from 'react';
import { CardHeader } from './CardHeader';
import { CardLink } from './CardLink';
import { usePermission } from '@backstage/plugin-permission-react';
import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha';
const useStyles = makeStyles<Theme>(theme => ({
box: {
@@ -103,6 +105,11 @@ export const TemplateCard = (props: TemplateCardProps) => {
!!props.additionalLinks?.length || !!template.metadata.links?.length;
const displayDefaultDivider = !hasTags && !hasLinks;
const { allowed: canCreateTask } = usePermission({
permission: taskCreatePermission,
resourceRef: 'task',
});
return (
<Card>
<CardHeader template={template} />
@@ -186,14 +193,16 @@ export const TemplateCard = (props: TemplateCardProps) => {
</>
)}
</div>
<Button
size="small"
variant="outlined"
color="primary"
onClick={() => props.onSelected?.(template)}
>
Choose
</Button>
{canCreateTask ? (
<Button
size="small"
variant="outlined"
color="primary"
onClick={() => props.onSelected?.(template)}
>
Choose
</Button>
) : null}
</div>
</CardActions>
</Card>
+2 -1
View File
@@ -23,7 +23,8 @@ import {
MockStarredEntitiesApi,
} from '@backstage/plugin-catalog-react';
import React from 'react';
import { scaffolderApiRef, ScaffolderClient } from '../src';
import { ScaffolderClient } from '../src';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import { ScaffolderPage } from '../src/plugin';
import {
discoveryApiRef,
@@ -43,7 +43,8 @@ import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
CodeSnippet,
Content,
ErrorPage,
EmptyState,
ErrorPanel,
Header,
MarkdownContent,
Page,
@@ -112,19 +113,9 @@ const ExamplesTable = (props: { examples: ActionExample[] }) => {
);
};
export const ActionsPage = () => {
const ActionPageContent = () => {
const api = useApi(scaffolderApiRef);
const navigate = useNavigate();
const editorLink = useRouteRef(editRouteRef);
const tasksLink = useRouteRef(scaffolderListTaskRouteRef);
const createLink = useRouteRef(rootRouteRef);
const scaffolderPageContextMenuProps = {
onEditorClicked: () => navigate(editorLink()),
onActionsClicked: undefined,
onTasksClicked: () => navigate(tasksLink()),
onCreateClicked: () => navigate(createLink()),
};
const classes = useStyles();
const { loading, value, error } = useAsync(async () => {
return api.listActions();
@@ -137,11 +128,14 @@ export const ActionsPage = () => {
if (error) {
return (
<ErrorPage
statusMessage="Failed to load installed actions"
status="500"
stack={error.stack}
/>
<>
<ErrorPanel error={error} />
<EmptyState
missing="info"
title="No information to display"
description="There are no actions installed or there was an issue communicating with backend."
/>
</>
);
}
@@ -282,7 +276,7 @@ export const ActionsPage = () => {
);
};
const items = value?.map(action => {
return value?.map(action => {
if (action.id.startsWith('legacy:')) {
return undefined;
}
@@ -336,6 +330,19 @@ export const ActionsPage = () => {
</Box>
);
});
};
export const ActionsPage = () => {
const navigate = useNavigate();
const editorLink = useRouteRef(editRouteRef);
const tasksLink = useRouteRef(scaffolderListTaskRouteRef);
const createLink = useRouteRef(rootRouteRef);
const scaffolderPageContextMenuProps = {
onEditorClicked: () => navigate(editorLink()),
onActionsClicked: undefined,
onTasksClicked: () => navigate(tasksLink()),
onCreateClicked: () => navigate(createLink()),
};
return (
<Page themeId="home">
@@ -346,7 +353,9 @@ export const ActionsPage = () => {
>
<ScaffolderPageContextMenu {...scaffolderPageContextMenuProps} />
</Header>
<Content>{items}</Content>
<Content>
<ActionPageContent />
</Content>
</Page>
);
};
@@ -77,7 +77,7 @@ const ListTaskPageContent = (props: MyTaskPageProps) => {
<EmptyState
missing="info"
title="No information to display"
description="There is no Tasks or there was an issue communicating with backend."
description="There are no tasks or there was an issue communicating with backend."
/>
</>
);
@@ -30,6 +30,12 @@ import MoreVert from '@material-ui/icons/MoreVert';
import React, { useState } from 'react';
import { useApi } from '@backstage/core-plugin-api';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import { usePermission } from '@backstage/plugin-permission-react';
import {
taskCancelPermission,
taskReadPermission,
taskCreatePermission,
} from '@backstage/plugin-scaffolder-common/alpha';
type ContextMenuProps = {
cancelEnabled?: boolean;
@@ -69,6 +75,28 @@ export const ContextMenu = (props: ContextMenuProps) => {
}
});
// Used dummy string value for `resourceRef` since `allowed` field will always return `false` if `resourceRef` is `undefined`
const { allowed: canCancelTask } = usePermission({
permission: taskCancelPermission,
resourceRef: 'task',
});
const { allowed: canReadTask } = usePermission({
permission: taskReadPermission,
resourceRef: 'task',
});
const { allowed: canCreateTask } = usePermission({
permission: taskCreatePermission,
resourceRef: 'task',
});
// Cancel endpoint requires user to have both read and cancel permissions
const cancelNotAllowed = !(canReadTask && canCancelTask);
// Start Over endpoint requires user to have both read (to grab parameters) and create (to create new task) permissions
const canStartOver = canReadTask && canCreateTask;
return (
<>
<IconButton
@@ -105,7 +133,11 @@ export const ContextMenu = (props: ContextMenuProps) => {
primary={buttonBarVisible ? 'Hide Button Bar' : 'Show Button Bar'}
/>
</MenuItem>
<MenuItem onClick={onStartOver}>
<MenuItem
onClick={onStartOver}
disabled={cancelEnabled || !canStartOver}
data-testid="start-over-task"
>
<ListItemIcon>
<Retry fontSize="small" />
</ListItemIcon>
@@ -113,7 +145,11 @@ export const ContextMenu = (props: ContextMenuProps) => {
</MenuItem>
<MenuItem
onClick={cancel}
disabled={!cancelEnabled || cancelStatus !== 'not-executed'}
disabled={
!cancelEnabled ||
cancelStatus !== 'not-executed' ||
cancelNotAllowed
}
data-testid="cancel-task"
>
<ListItemIcon>
@@ -16,10 +16,20 @@
import { OngoingTask } from './OngoingTask';
import React from 'react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import {
renderInTestApp,
TestApiProvider,
MockPermissionApi,
} from '@backstage/test-utils';
import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
import { act, fireEvent, waitFor, within } from '@testing-library/react';
import {
PermissionApi,
permissionApiRef,
} from '@backstage/plugin-permission-react';
import { rootRouteRef } from '../../routes';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { SWRConfig } from 'swr';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -49,18 +59,29 @@ describe('OngoingTask', () => {
getTask: jest.fn().mockImplementation(async () => {}),
};
beforeEach(() => {
beforeEach(async () => {
jest.clearAllMocks();
});
it('should trigger cancel api on "Cancel" click in context menu', async () => {
const cancelOptionLabel = 'Cancel';
const rendered = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
<OngoingTask />
</TestApiProvider>,
const render = (permissionApi?: PermissionApi) => {
// SWR used by the usePermission hook needs cache to be reset for each test
return renderInTestApp(
<SWRConfig value={{ provider: () => new Map() }}>
<TestApiProvider
apis={[
[scaffolderApiRef, mockScaffolderApi],
[permissionApiRef, permissionApi || new MockPermissionApi()],
]}
>
<OngoingTask />
</TestApiProvider>
</SWRConfig>,
{ mountedRoutes: { '/': rootRouteRef } },
);
};
it('should trigger cancel api on "Cancel" click in context menu', async () => {
const rendered = await render();
const cancelOptionLabel = 'Cancel';
const { getByTestId } = rendered;
await act(async () => {
@@ -84,13 +105,9 @@ describe('OngoingTask', () => {
});
it('should trigger cancel api on "Cancel" button click', async () => {
const rendered = await render();
const cancelOptionLabel = 'Cancel';
const rendered = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
<OngoingTask />
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
);
const { getByTestId } = rendered;
await act(async () => {
@@ -114,22 +131,12 @@ describe('OngoingTask', () => {
});
it('should initially do not display logs', async () => {
const rendered = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
<OngoingTask />
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
);
const rendered = await render();
await expect(rendered.findByText('Show Logs')).resolves.toBeInTheDocument();
});
it('should toggle logs visibility', async () => {
const rendered = await renderInTestApp(
<TestApiProvider apis={[[scaffolderApiRef, mockScaffolderApi]]}>
<OngoingTask />
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
);
const rendered = await render();
await act(async () => {
const element = await rendered.findByText('Show Logs');
fireEvent.click(element);
@@ -137,4 +144,22 @@ describe('OngoingTask', () => {
await expect(rendered.findByText('Hide Logs')).resolves.toBeInTheDocument();
});
it('should have cancel and start over buttons be disabled without the proper permissions', async () => {
const mockAuthorize = jest
.fn()
.mockImplementation(async () => ({ result: AuthorizeResult.DENY }));
const permissionApi: PermissionApi = { authorize: mockAuthorize };
const rendered = await render(permissionApi);
const { getByTestId } = rendered;
expect(getByTestId('cancel-button')).toHaveClass('Mui-disabled');
expect(getByTestId('start-over-button')).toHaveClass('Mui-disabled');
await act(async () => {
fireEvent.click(getByTestId('menu-button'));
});
expect(getByTestId('cancel-task')).toHaveClass('Mui-disabled');
expect(getByTestId('start-over-task')).toHaveClass('Mui-disabled');
});
});
@@ -35,6 +35,12 @@ import {
TaskSteps,
} from '@backstage/plugin-scaffolder-react/alpha';
import { useAsync } from '@react-hookz/web';
import { usePermission } from '@backstage/plugin-permission-react';
import {
taskCancelPermission,
taskReadPermission,
taskCreatePermission,
} from '@backstage/plugin-scaffolder-common/alpha';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -81,6 +87,28 @@ export const OngoingTask = (props: {
const [logsVisible, setLogVisibleState] = useState(false);
const [buttonBarVisible, setButtonBarVisibleState] = useState(true);
// Used dummy string value for `resourceRef` since `allowed` field will always return `false` if `resourceRef` is `undefined`
const { allowed: canCancelTask } = usePermission({
permission: taskCancelPermission,
resourceRef: 'task',
});
const { allowed: canReadTask } = usePermission({
permission: taskReadPermission,
resourceRef: 'task',
});
const { allowed: canCreateTask } = usePermission({
permission: taskCreatePermission,
resourceRef: 'task',
});
// Cancel endpoint requires user to have both read and cancel permissions
const cancelNotAllowed = !(canReadTask && canCancelTask);
// Start Over endpoint requires user to have both read (to grab parameters) and create (to create new task) permissions
const canStartOver = canReadTask && canCreateTask;
useEffect(() => {
if (taskStream.error) {
setLogVisibleState(true);
@@ -192,7 +220,11 @@ export const OngoingTask = (props: {
<div className={classes.buttonBar}>
<Button
className={classes.cancelButton}
disabled={!cancelEnabled || cancelStatus !== 'not-executed'}
disabled={
!cancelEnabled ||
cancelStatus !== 'not-executed' ||
cancelNotAllowed
}
onClick={triggerCancel}
data-testid="cancel-button"
>
@@ -209,8 +241,9 @@ export const OngoingTask = (props: {
<Button
variant="contained"
color="primary"
disabled={cancelEnabled}
disabled={cancelEnabled || !canStartOver}
onClick={startOver}
data-testid="start-over-button"
>
Start Over
</Button>