Merge pull request #8055 from mabashian/8052-workflow-viz-rbac

Fixes some rbac issues in the workflow toolbar and start screen

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-09-14 21:14:12 +00:00 committed by GitHub
commit a90bb36b72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 190 additions and 73 deletions

View File

@ -41,7 +41,7 @@ function getNodeType(node) {
}
}
function NodeViewModal({ i18n }) {
function NodeViewModal({ i18n, readOnly }) {
const dispatch = useContext(WorkflowDispatchContext);
const { nodeToView } = useContext(WorkflowStateContext);
const { unifiedJobTemplate } = nodeToView;
@ -136,15 +136,20 @@ function NodeViewModal({ i18n }) {
title={unifiedJobTemplate.name}
aria-label={i18n._(t`Workflow node view modal`)}
onClose={() => dispatch({ type: 'SET_NODE_TO_VIEW', value: null })}
actions={[
<Button
key="edit"
aria-label={i18n._(t`Edit Node`)}
onClick={handleEdit}
>
{i18n._(t`Edit`)}
</Button>,
]}
actions={
readOnly
? []
: [
<Button
id="node-view-edit-button"
key="edit"
aria-label={i18n._(t`Edit Node`)}
onClick={handleEdit}
>
{i18n._(t`Edit`)}
</Button>,
]
}
>
{Content}
</Modal>

View File

@ -168,6 +168,40 @@ describe('NodeViewModal', () => {
wrapper.unmount();
jest.clearAllMocks();
});
test('edit button shoud be shown when readOnly prop is false', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider value={workflowContext}>
<NodeViewModal />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
});
waitForLoaded(wrapper);
expect(wrapper.find('Button#node-view-edit-button').length).toBe(1);
wrapper.unmount();
jest.clearAllMocks();
});
test('edit button shoud be hidden when readOnly prop is true', async () => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<WorkflowDispatchContext.Provider value={dispatch}>
<WorkflowStateContext.Provider value={workflowContext}>
<NodeViewModal readOnly />
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
);
});
waitForLoaded(wrapper);
expect(wrapper.find('Button#node-view-edit-button').length).toBe(0);
wrapper.unmount();
jest.clearAllMocks();
});
});
describe('Project node', () => {

View File

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

View File

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

View File

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

View File

@ -19,4 +19,12 @@ describe('VisualizerStartScreen', () => {
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,
template,
hasUnsavedChanges,
readOnly,
}) {
const dispatch = useContext(WorkflowDispatchContext);
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
const canLaunch =
template.summary_fields?.user_capabilities?.start && !hasUnsavedChanges;
return (
<div id="visualizer-toolbar">
@ -112,43 +111,49 @@ function VisualizerToolbar({
>
<BookIcon />
</ActionButton>
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
{({ handleLaunch }) => (
<ActionButton
id="visualizer-launch"
variant="plain"
isDisabled={!canLaunch || totalNodes === 0}
onClick={handleLaunch}
{template.summary_fields?.user_capabilities?.start && (
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
{({ handleLaunch }) => (
<ActionButton
id="visualizer-launch"
variant="plain"
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 />
</ActionButton>
)}
</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>
{i18n._(t`Save`)}
</Button>
</>
)}
<Button
id="visualizer-close"
aria-label={i18n._(t`Close`)}
@ -168,6 +173,7 @@ VisualizerToolbar.propTypes = {
onSave: func.isRequired,
template: shape().isRequired,
hasUnsavedChanges: bool.isRequired,
readOnly: bool.isRequired,
};
export default withI18n()(VisualizerToolbar);

View File

@ -47,6 +47,7 @@ describe('VisualizerToolbar', () => {
onSave={save}
template={template}
hasUnsavedChanges={false}
readOnly={false}
/>
</WorkflowStateContext.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', () => {
expect(wrapper.find('LaunchButton button').prop('disabled')).toEqual(false);
const nodes = [
@ -111,6 +151,7 @@ describe('VisualizerToolbar', () => {
onSave={save}
template={template}
hasUnsavedChanges
readOnly={false}
/>
</WorkflowStateContext.Provider>
</WorkflowDispatchContext.Provider>
@ -120,13 +161,26 @@ describe('VisualizerToolbar', () => {
).toEqual(true);
});
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('Buttons should be hidden when user cannot edit workflow', () => {
const nodes = [
{
id: 1,
},
];
const toolbar = mountWithContexts(
<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);
});
});