mirror of
https://github.com/ansible/awx.git
synced 2026-03-13 15:09:32 -02:30
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:
@@ -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 />}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user