Fixes some rbac issues in the workflow toolbar and start screen. Add/edit-related buttons should be hidden for users that cannot edit the workflow.

This commit is contained in:
mabashian
2020-09-01 13:32:28 -04:00
parent a3e08a3d09
commit 945dfbb648
6 changed files with 140 additions and 62 deletions

View File

@@ -424,6 +424,8 @@ function Visualizer({ template, i18n }) {
); );
} }
const readOnly = !template?.summary_fields?.user_capabilities?.edit;
return ( return (
<WorkflowStateContext.Provider value={state}> <WorkflowStateContext.Provider value={state}>
<WorkflowDispatchContext.Provider value={dispatch}> <WorkflowDispatchContext.Provider value={dispatch}>
@@ -433,13 +435,12 @@ function Visualizer({ template, i18n }) {
onSave={handleVisualizerSave} onSave={handleVisualizerSave}
hasUnsavedChanges={unsavedChanges} hasUnsavedChanges={unsavedChanges}
template={template} template={template}
readOnly={readOnly}
/> />
{links.length > 0 ? ( {links.length > 0 ? (
<VisualizerGraph <VisualizerGraph readOnly={readOnly} />
readOnly={!template.summary_fields.user_capabilities.edit}
/>
) : ( ) : (
<VisualizerStartScreen /> <VisualizerStartScreen readOnly={readOnly} />
)} )}
</Wrapper> </Wrapper>
{nodeToDelete && <NodeDeleteModal />} {nodeToDelete && <NodeDeleteModal />}

View File

@@ -271,6 +271,7 @@ function VisualizerGraph({ i18n, readOnly }) {
key="start" key="start"
showActionTooltip={!readOnly} showActionTooltip={!readOnly}
onUpdateHelpText={setHelpText} onUpdateHelpText={setHelpText}
readOnly={readOnly}
/>, />,
links.map(link => { links.map(link => {
if ( if (

View File

@@ -30,23 +30,31 @@ const StartPanelWrapper = styled.div`
justify-content: center; justify-content: center;
`; `;
function VisualizerStartScreen({ i18n }) { function VisualizerStartScreen({ i18n, readOnly }) {
const dispatch = useContext(WorkflowDispatchContext); const dispatch = useContext(WorkflowDispatchContext);
return ( return (
<div css="flex: 1"> <div css="flex: 1">
<StartPanelWrapper> <StartPanelWrapper>
<StartPanel> <StartPanel>
<p>{i18n._(t`Please click the Start button to begin.`)}</p> {readOnly ? (
<Button <p>
id="visualizer-start" {i18n._(t`This workflow does not have any nodes configured.`)}
aria-label={i18n._(t`Start`)} </p>
onClick={() => ) : (
dispatch({ type: 'START_ADD_NODE', sourceNodeId: 1 }) <>
} <p>{i18n._(t`Please click the Start button to begin.`)}</p>
variant="primary" <Button
> id="visualizer-start"
{i18n._(t`Start`)} aria-label={i18n._(t`Start`)}
</Button> onClick={() =>
dispatch({ type: 'START_ADD_NODE', sourceNodeId: 1 })
}
variant="primary"
>
{i18n._(t`Start`)}
</Button>
</>
)}
</StartPanel> </StartPanel>
</StartPanelWrapper> </StartPanelWrapper>
</div> </div>

View File

@@ -19,4 +19,12 @@ describe('VisualizerStartScreen', () => {
sourceNodeId: 1, sourceNodeId: 1,
}); });
}); });
test('start button hidden in read-only mode', () => {
const wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<VisualizerStartScreen readOnly />
</WorkflowDispatchContext.Provider>
);
expect(wrapper.find('Button').length).toBe(0);
});
}); });

View File

@@ -56,14 +56,13 @@ function VisualizerToolbar({
onSave, onSave,
template, template,
hasUnsavedChanges, hasUnsavedChanges,
readOnly,
}) { }) {
const dispatch = useContext(WorkflowDispatchContext); const dispatch = useContext(WorkflowDispatchContext);
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext); const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1; const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
const canLaunch =
template.summary_fields?.user_capabilities?.start && !hasUnsavedChanges;
return ( return (
<div id="visualizer-toolbar"> <div id="visualizer-toolbar">
@@ -112,43 +111,49 @@ function VisualizerToolbar({
> >
<BookIcon /> <BookIcon />
</ActionButton> </ActionButton>
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}> {template.summary_fields?.user_capabilities?.start && (
{({ handleLaunch }) => ( <LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
<ActionButton {({ handleLaunch }) => (
id="visualizer-launch" <ActionButton
variant="plain" id="visualizer-launch"
isDisabled={!canLaunch || totalNodes === 0} variant="plain"
onClick={handleLaunch} isDisabled={hasUnsavedChanges || totalNodes === 0}
onClick={handleLaunch}
>
<RocketIcon />
</ActionButton>
)}
</LaunchButton>
)}
{!readOnly && (
<>
<Tooltip content={i18n._(t`Delete All Nodes`)} position="bottom">
<ActionButton
id="visualizer-delete-all"
aria-label={i18n._(t`Delete all nodes`)}
isDisabled={totalNodes === 0}
onClick={() =>
dispatch({
type: 'SET_SHOW_DELETE_ALL_NODES_MODAL',
value: true,
})
}
variant="plain"
>
<TrashAltIcon />
</ActionButton>
</Tooltip>
<Button
id="visualizer-save"
css="margin: 0 32px"
aria-label={i18n._(t`Save`)}
variant="primary"
onClick={onSave}
> >
<RocketIcon /> {i18n._(t`Save`)}
</ActionButton> </Button>
)} </>
</LaunchButton> )}
<Tooltip content={i18n._(t`Delete All Nodes`)} position="bottom">
<ActionButton
id="visualizer-delete-all"
aria-label={i18n._(t`Delete all nodes`)}
isDisabled={totalNodes === 0}
onClick={() =>
dispatch({
type: 'SET_SHOW_DELETE_ALL_NODES_MODAL',
value: true,
})
}
variant="plain"
>
<TrashAltIcon />
</ActionButton>
</Tooltip>
<Button
id="visualizer-save"
css="margin: 0 32px"
aria-label={i18n._(t`Save`)}
variant="primary"
onClick={onSave}
>
{i18n._(t`Save`)}
</Button>
<Button <Button
id="visualizer-close" id="visualizer-close"
aria-label={i18n._(t`Close`)} aria-label={i18n._(t`Close`)}
@@ -168,6 +173,7 @@ VisualizerToolbar.propTypes = {
onSave: func.isRequired, onSave: func.isRequired,
template: shape().isRequired, template: shape().isRequired,
hasUnsavedChanges: bool.isRequired, hasUnsavedChanges: bool.isRequired,
readOnly: bool.isRequired,
}; };
export default withI18n()(VisualizerToolbar); export default withI18n()(VisualizerToolbar);

View File

@@ -47,6 +47,7 @@ describe('VisualizerToolbar', () => {
onSave={save} onSave={save}
template={template} template={template}
hasUnsavedChanges={false} hasUnsavedChanges={false}
readOnly={false}
/> />
</WorkflowStateContext.Provider> </WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider> </WorkflowDispatchContext.Provider>
@@ -96,6 +97,45 @@ describe('VisualizerToolbar', () => {
}); });
}); });
test('Save button calls expected function', () => {
wrapper.find('button[aria-label="Save"]').simulate('click');
expect(save).toHaveBeenCalled();
});
test('Close button calls expected function', () => {
wrapper.find('TimesIcon').simulate('click');
expect(close).toHaveBeenCalled();
});
test('Launch button should be hidden when user cannot start workflow', () => {
const nodes = [
{
id: 1,
},
];
const toolbar = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider value={{ ...workflowContext, nodes }}>
<VisualizerToolbar
onClose={close}
onSave={save}
template={{
...template,
summary_fields: {
user_capabilities: {
start: false,
},
},
}}
hasUnsavedChanges
readOnly={false}
/>
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
expect(toolbar.find('LaunchButton button').length).toBe(0);
});
test('Launch button should be disabled when there are unsaved changes', () => { test('Launch button should be disabled when there are unsaved changes', () => {
expect(wrapper.find('LaunchButton button').prop('disabled')).toEqual(false); expect(wrapper.find('LaunchButton button').prop('disabled')).toEqual(false);
const nodes = [ const nodes = [
@@ -111,6 +151,7 @@ describe('VisualizerToolbar', () => {
onSave={save} onSave={save}
template={template} template={template}
hasUnsavedChanges hasUnsavedChanges
readOnly={false}
/> />
</WorkflowStateContext.Provider> </WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider> </WorkflowDispatchContext.Provider>
@@ -120,13 +161,26 @@ describe('VisualizerToolbar', () => {
).toEqual(true); ).toEqual(true);
}); });
test('Save button calls expected function', () => { test('Buttons should be hidden when user cannot edit workflow', () => {
wrapper.find('button[aria-label="Save"]').simulate('click'); const nodes = [
expect(save).toHaveBeenCalled(); {
}); id: 1,
},
test('Close button calls expected function', () => { ];
wrapper.find('TimesIcon').simulate('click'); const toolbar = mountWithContexts(
expect(close).toHaveBeenCalled(); <WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider value={{ ...workflowContext, nodes }}>
<VisualizerToolbar
onClose={close}
onSave={save}
template={template}
hasUnsavedChanges={false}
readOnly
/>
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
expect(toolbar.find('#visualizer-delete-all').length).toBe(0);
expect(toolbar.find('#visualizer-save').length).toBe(0);
}); });
}); });