mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 18:40:01 -03:30
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:
commit
a90bb36b72
@ -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>
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -271,6 +271,7 @@ function VisualizerGraph({ i18n, readOnly }) {
|
||||
key="start"
|
||||
showActionTooltip={!readOnly}
|
||||
onUpdateHelpText={setHelpText}
|
||||
readOnly={readOnly}
|
||||
/>,
|
||||
links.map(link => {
|
||||
if (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user