mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 11:00:03 -03:30
Stabilized workflow visualizer and output point. Workflow jobs can be viewed and workflows can be built (without jt prompting).
This commit is contained in:
parent
50ba4f9759
commit
1d84d03566
@ -2,10 +2,10 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import SelectableCard from '@components/SelectableCard';
|
||||
import Wizard from '@components/Wizard';
|
||||
import SelectResourceStep from './SelectResourceStep';
|
||||
import SelectRoleStep from './SelectRoleStep';
|
||||
import { SelectableCard } from '@components/SelectableCard';
|
||||
import { Wizard } from '@components/Wizard';
|
||||
import { TeamsAPI, UsersAPI } from '../../api';
|
||||
|
||||
const readUsers = async queryParams =>
|
||||
|
||||
@ -72,6 +72,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<Detail
|
||||
alwaysVisible={false}
|
||||
fullWidth={false}
|
||||
label="Name"
|
||||
value="jane brown"
|
||||
@ -83,6 +84,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<Detail
|
||||
alwaysVisible={false}
|
||||
fullWidth={false}
|
||||
label="Team Roles"
|
||||
value={
|
||||
@ -133,6 +135,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<Detail
|
||||
alwaysVisible={false}
|
||||
fullWidth={false}
|
||||
label="Name"
|
||||
value="jane brown"
|
||||
@ -144,6 +147,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<Detail
|
||||
alwaysVisible={false}
|
||||
fullWidth={false}
|
||||
label="Team Roles"
|
||||
value={
|
||||
@ -217,6 +221,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<Detail
|
||||
alwaysVisible={false}
|
||||
fullWidth={false}
|
||||
label="Name"
|
||||
value="jane brown"
|
||||
@ -228,6 +233,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
stacked={true}
|
||||
>
|
||||
<Detail
|
||||
alwaysVisible={false}
|
||||
fullWidth={false}
|
||||
label="Team Roles"
|
||||
value={
|
||||
@ -400,6 +406,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
data-pf-content={true}
|
||||
>
|
||||
<Detail
|
||||
alwaysVisible={false}
|
||||
fullWidth={false}
|
||||
label="Name"
|
||||
value="jane brown"
|
||||
@ -573,6 +580,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
||||
data-pf-content={true}
|
||||
>
|
||||
<Detail
|
||||
alwaysVisible={false}
|
||||
fullWidth={false}
|
||||
label="Team Roles"
|
||||
value={
|
||||
|
||||
@ -52,6 +52,7 @@ function SelectableCard({ label, description, onClick, isSelected, dataCy }) {
|
||||
|
||||
SelectableCard.propTypes = {
|
||||
label: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
};
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default as SelectableCard } from './SelectableCard';
|
||||
export { default } from './SelectableCard';
|
||||
|
||||
@ -4,7 +4,12 @@ import Wizard from './Wizard';
|
||||
|
||||
describe('Wizard', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mount(<Wizard />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const wrapper = mount(
|
||||
<Wizard
|
||||
title="Simple Wizard"
|
||||
steps={[{ name: 'Step 1', component: <p>Step 1</p> }]}
|
||||
/>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1 +1 @@
|
||||
export { default as Wizard } from './Wizard';
|
||||
export { default } from './Wizard';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { node, number } from 'prop-types';
|
||||
|
||||
const TooltipContents = styled.div`
|
||||
display: flex;
|
||||
@ -10,32 +11,32 @@ const TooltipArrows = styled.div`
|
||||
`;
|
||||
|
||||
const TooltipArrowOuter = styled.div`
|
||||
border-bottom: 10px solid transparent;
|
||||
border-right: 10px solid #c4c4c4;
|
||||
border-top: 10px solid transparent;
|
||||
height: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: calc(50% - 10px);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-right: 10px solid #c4c4c4;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid transparent;
|
||||
margin: auto;
|
||||
`;
|
||||
|
||||
const TooltipArrowInner = styled.div`
|
||||
position: absolute;
|
||||
top: calc(50% - 10px);
|
||||
left: 2px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 10px solid transparent;
|
||||
border-right: 10px solid white;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid transparent;
|
||||
height: 0;
|
||||
left: 2px;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: calc(50% - 10px);
|
||||
width: 0;
|
||||
`;
|
||||
|
||||
const TooltipActions = styled.div`
|
||||
background-color: white;
|
||||
border: 1px solid #c4c4c4;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #c4c4c4;
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
@ -59,4 +60,10 @@ function WorkflowActionTooltip({ actions, pointX, pointY }) {
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowActionTooltip.propTypes = {
|
||||
actions: node.isRequired,
|
||||
pointX: number.isRequired,
|
||||
pointY: number.isRequired,
|
||||
};
|
||||
|
||||
export default WorkflowActionTooltip;
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { func } from 'prop-types';
|
||||
|
||||
const TooltipItem = styled.div`
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
height: 25px;
|
||||
justify-content: center;
|
||||
width: 25px;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
@ -21,21 +22,33 @@ const TooltipItem = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
function WorkflowActionTooltip({
|
||||
function WorkflowActionTooltipItem({
|
||||
children,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onClick,
|
||||
}) {
|
||||
return (
|
||||
<TooltipItem
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</TooltipItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowActionTooltip;
|
||||
WorkflowActionTooltipItem.propTypes = {
|
||||
onClick: func,
|
||||
onMouseEnter: func,
|
||||
onMouseLeave: func,
|
||||
};
|
||||
|
||||
WorkflowActionTooltipItem.defaultProps = {
|
||||
onClick: () => {},
|
||||
onMouseEnter: () => {},
|
||||
onMouseLeave: () => {},
|
||||
};
|
||||
|
||||
export default WorkflowActionTooltipItem;
|
||||
|
||||
@ -2,20 +2,20 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Outer = styled.div`
|
||||
position: relative;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Inner = styled.div`
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
background-color: #383f44;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 2px;
|
||||
color: white;
|
||||
left: 10px;
|
||||
max-width: 300px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
`;
|
||||
|
||||
function WorkflowHelp({ children }) {
|
||||
|
||||
@ -5,50 +5,50 @@ import styled from 'styled-components';
|
||||
import { ExclamationTriangleIcon, PauseIcon } from '@patternfly/react-icons';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border: 1px solid #c7c7c7;
|
||||
background-color: white;
|
||||
min-width: 100px;
|
||||
border: 1px solid #c7c7c7;
|
||||
margin-left: 20px;
|
||||
min-width: 100px;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #c7c7c7;
|
||||
padding: 10px;
|
||||
`;
|
||||
|
||||
const Key = styled.ul`
|
||||
padding: 5px 10px;
|
||||
|
||||
li {
|
||||
padding: 5px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const NodeTypeLetter = styled.div`
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
background-color: #393f43;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
line-height: 20px;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
`;
|
||||
|
||||
const StyledExclamationTriangleIcon = styled(ExclamationTriangleIcon)`
|
||||
color: #f0ad4d;
|
||||
margin-right: 10px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
`;
|
||||
|
||||
const Link = styled.div`
|
||||
height: 5px;
|
||||
width: 20px;
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
`;
|
||||
|
||||
const SuccessLink = styled(Link)`
|
||||
@ -63,7 +63,7 @@ const AlwaysLink = styled(Link)`
|
||||
background-color: #337ab7;
|
||||
`;
|
||||
|
||||
function VisualizerKey({ i18n }) {
|
||||
function WorkflowKey({ i18n }) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Header>
|
||||
@ -113,4 +113,4 @@ function VisualizerKey({ i18n }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(VisualizerKey);
|
||||
export default withI18n()(WorkflowKey);
|
||||
@ -2,11 +2,12 @@ import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { shape } from 'prop-types';
|
||||
|
||||
const GridDL = styled.dl`
|
||||
column-gap: 15px;
|
||||
display: grid;
|
||||
grid-template-columns: max-content;
|
||||
column-gap: 15px;
|
||||
row-gap: 0px;
|
||||
dt {
|
||||
grid-column-start: 1;
|
||||
@ -18,7 +19,7 @@ const GridDL = styled.dl`
|
||||
|
||||
function WorkflowLinkHelp({ link, i18n }) {
|
||||
let linkType;
|
||||
switch (link.edgeType) {
|
||||
switch (link.linkType) {
|
||||
case 'always':
|
||||
linkType = i18n._(t`Always`);
|
||||
break;
|
||||
@ -42,4 +43,8 @@ function WorkflowLinkHelp({ link, i18n }) {
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowLinkHelp.propTypes = {
|
||||
link: shape().isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(WorkflowLinkHelp);
|
||||
|
||||
@ -2,12 +2,13 @@ import React, { Fragment } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { shape } from 'prop-types';
|
||||
import { secondsToHHMMSS } from '@util/dates';
|
||||
|
||||
const GridDL = styled.dl`
|
||||
column-gap: 15px;
|
||||
display: grid;
|
||||
grid-template-columns: max-content;
|
||||
column-gap: 15px;
|
||||
row-gap: 0px;
|
||||
dt {
|
||||
grid-column-start: 1;
|
||||
@ -134,4 +135,8 @@ function WorkflowNodeHelp({ node, i18n }) {
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowNodeHelp.propTypes = {
|
||||
node: shape().isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(WorkflowNodeHelp);
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { shape } from 'prop-types';
|
||||
import { PauseIcon } from '@patternfly/react-icons';
|
||||
|
||||
const NodeTypeLetter = styled.foreignObject`
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
background-color: #393f43;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
function WorkflowNodeTypeLetter({ node }) {
|
||||
@ -52,4 +53,8 @@ function WorkflowNodeTypeLetter({ node }) {
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowNodeTypeLetter.propTypes = {
|
||||
node: shape().isRequired,
|
||||
};
|
||||
|
||||
export default WorkflowNodeTypeLetter;
|
||||
|
||||
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { func, number } from 'prop-types';
|
||||
import { Tooltip } from '@patternfly/react-core';
|
||||
import {
|
||||
CaretDownIcon,
|
||||
@ -15,19 +16,19 @@ import {
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border: 1px solid #c7c7c7;
|
||||
background-color: white;
|
||||
border: 1px solid #c7c7c7;
|
||||
height: 135px;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #c7c7c7;
|
||||
padding: 10px;
|
||||
`;
|
||||
|
||||
const Pan = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const PanCenter = styled.div`
|
||||
@ -36,18 +37,18 @@ const PanCenter = styled.div`
|
||||
`;
|
||||
|
||||
const Tools = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
function VisualizerTools({
|
||||
function WorkflowTools({
|
||||
i18n,
|
||||
zoomPercentage,
|
||||
onZoomChange,
|
||||
onFitGraph,
|
||||
onPan,
|
||||
onPanToMiddle,
|
||||
onZoomChange,
|
||||
zoomPercentage,
|
||||
}) {
|
||||
const zoomIn = () => {
|
||||
const newScale =
|
||||
@ -81,14 +82,16 @@ function VisualizerTools({
|
||||
<MinusIcon onClick={() => zoomOut()} css="margin-right: 10px;" />
|
||||
</Tooltip>
|
||||
<input
|
||||
type="range"
|
||||
id="zoom-slider"
|
||||
value={zoomPercentage}
|
||||
min="10"
|
||||
max="200"
|
||||
min="10"
|
||||
onChange={event =>
|
||||
onZoomChange(parseInt(event.target.value, 10) / 100)
|
||||
}
|
||||
step="10"
|
||||
onChange={event => onZoomChange(parseInt(event.target.value) / 100)}
|
||||
></input>
|
||||
type="range"
|
||||
value={zoomPercentage}
|
||||
/>
|
||||
<Tooltip content={i18n._(t`Zoom In`)} position="bottom">
|
||||
<PlusIcon onClick={() => zoomIn()} css="margin: 0px 25px 0px 10px;" />
|
||||
</Tooltip>
|
||||
@ -119,4 +122,12 @@ function VisualizerTools({
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(VisualizerTools);
|
||||
WorkflowTools.propTypes = {
|
||||
onFitGraph: func.isRequired,
|
||||
onPan: func.isRequired,
|
||||
onPanToMiddle: func.isRequired,
|
||||
onZoomChange: func.isRequired,
|
||||
zoomPercentage: number.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(WorkflowTools);
|
||||
@ -1,8 +1,10 @@
|
||||
export { default as WorkflowHelp } from './WorkflowHelp';
|
||||
export { default as WorkflowLinkHelp } from './WorkflowLinkHelp';
|
||||
export { default as WorkflowNodeHelp } from './WorkflowNodeHelp';
|
||||
export { default as WorkflowNodeTypeLetter } from './WorkflowNodeTypeLetter';
|
||||
export { default as WorkflowActionTooltip } from './WorkflowActionTooltip';
|
||||
export {
|
||||
default as WorkflowActionTooltipItem,
|
||||
} from './WorkflowActionTooltipItem';
|
||||
export { default as WorkflowHelp } from './WorkflowHelp';
|
||||
export { default as WorkflowKey } from './WorkflowKey';
|
||||
export { default as WorkflowLinkHelp } from './WorkflowLinkHelp';
|
||||
export { default as WorkflowNodeHelp } from './WorkflowNodeHelp';
|
||||
export { default as WorkflowNodeTypeLetter } from './WorkflowNodeTypeLetter';
|
||||
export { default as WorkflowTools } from './WorkflowTools';
|
||||
|
||||
@ -25,16 +25,6 @@ import {
|
||||
InventoriesAPI,
|
||||
AdHocCommandsAPI,
|
||||
} from '@api';
|
||||
import { JOB_TYPE_URL_SEGMENTS } from '@constants';
|
||||
|
||||
const ActionButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
& > :not(:first-child) {
|
||||
margin-left: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const VariablesInput = styled(_VariablesInput)`
|
||||
.pf-c-form__label {
|
||||
|
||||
@ -2,64 +2,60 @@ import React, { useState, useEffect } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { shape } from 'prop-types';
|
||||
import { CardBody as PFCardBody } from '@patternfly/react-core';
|
||||
import { layoutGraph } from '@util/workflow';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { WorkflowJobsAPI } from '@api';
|
||||
import WorkflowOutputGraph from './WorkflowOutputGraph';
|
||||
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
||||
|
||||
const CardBody = styled(PFCardBody)`
|
||||
height: calc(100vh - 240px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const Toolbar = styled.div`
|
||||
height: 50px;
|
||||
background-color: grey;
|
||||
height: calc(100vh - 240px);
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const fetchWorkflowNodes = async (jobId, pageNo = 1, nodes = []) => {
|
||||
try {
|
||||
const { data } = await WorkflowJobsAPI.readNodes(jobId, {
|
||||
page_size: 200,
|
||||
page: pageNo,
|
||||
});
|
||||
if (data.next) {
|
||||
return await fetchWorkflowNodes(
|
||||
jobId,
|
||||
pageNo + 1,
|
||||
nodes.concat(data.results)
|
||||
);
|
||||
}
|
||||
return nodes.concat(data.results);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
const { data } = await WorkflowJobsAPI.readNodes(jobId, {
|
||||
page_size: 200,
|
||||
page: pageNo,
|
||||
});
|
||||
if (data.next) {
|
||||
return fetchWorkflowNodes(
|
||||
jobId,
|
||||
pageNo + 1,
|
||||
nodes.concat(data.results)
|
||||
);
|
||||
}
|
||||
return nodes.concat(data.results);
|
||||
};
|
||||
|
||||
function WorkflowOutput({ job, i18n }) {
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [graphLinks, setGraphLinks] = useState([]);
|
||||
const [graphNodes, setGraphNodes] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [nodePositions, setNodePositions] = useState(null);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [showTools, setShowTools] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const buildGraphArrays = nodes => {
|
||||
const nonRootNodeIds = [];
|
||||
const allNodeIds = [];
|
||||
const arrayOfLinksForChart = [];
|
||||
const nodeIdToChartNodeIdMapping = {};
|
||||
const chartNodeIdToIndexMapping = {};
|
||||
const nodeIdToChartNodeIdMapping = {};
|
||||
const nodeRef = {};
|
||||
const nonRootNodeIds = [];
|
||||
let nodeIdCounter = 1;
|
||||
const arrayOfNodesForChart = [
|
||||
{
|
||||
@ -110,7 +106,7 @@ function WorkflowOutput({ job, i18n }) {
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
edgeType: 'success',
|
||||
linkType: 'success',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
@ -121,7 +117,7 @@ function WorkflowOutput({ job, i18n }) {
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
edgeType: 'failure',
|
||||
linkType: 'failure',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
@ -132,7 +128,7 @@ function WorkflowOutput({ job, i18n }) {
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
edgeType: 'always',
|
||||
linkType: 'always',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
@ -151,7 +147,7 @@ function WorkflowOutput({ job, i18n }) {
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[0],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
edgeType: 'always',
|
||||
linkType: 'always',
|
||||
type: 'link',
|
||||
});
|
||||
});
|
||||
@ -206,12 +202,21 @@ function WorkflowOutput({ job, i18n }) {
|
||||
return (
|
||||
<CardBody>
|
||||
<Wrapper>
|
||||
<Toolbar>Toolbar</Toolbar>
|
||||
<WorkflowOutputToolbar
|
||||
job={job}
|
||||
keyShown={showKey}
|
||||
nodes={graphNodes}
|
||||
onKeyToggle={() => setShowKey(!showKey)}
|
||||
onToolsToggle={() => setShowTools(!showTools)}
|
||||
toolsShown={showTools}
|
||||
/>
|
||||
{nodePositions && (
|
||||
<WorkflowOutputGraph
|
||||
links={graphLinks}
|
||||
nodes={graphNodes}
|
||||
nodePositions={nodePositions}
|
||||
nodes={graphNodes}
|
||||
showKey={showKey}
|
||||
showTools={showTools}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
@ -219,4 +224,8 @@ function WorkflowOutput({ job, i18n }) {
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowOutput.propTypes = {
|
||||
job: shape().isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(WorkflowOutput);
|
||||
|
||||
@ -1,15 +1,28 @@
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import { WorkflowHelp, WorkflowNodeHelp } from '@components/Workflow';
|
||||
import { calcZoomAndFit } from '@util/workflow';
|
||||
import { arrayOf, bool, shape } from 'prop-types';
|
||||
import { calcZoomAndFit, getZoomTranslate } from '@util/workflow';
|
||||
import {
|
||||
WorkflowOutputLink,
|
||||
WorkflowOutputNode,
|
||||
WorkflowOutputStartNode,
|
||||
} from '@screens/Job/WorkflowOutput';
|
||||
import {
|
||||
WorkflowHelp,
|
||||
WorkflowKey,
|
||||
WorkflowNodeHelp,
|
||||
WorkflowTools,
|
||||
} from '@components/Workflow';
|
||||
|
||||
function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
||||
function WorkflowOutputGraph({
|
||||
links,
|
||||
nodePositions,
|
||||
nodes,
|
||||
showKey,
|
||||
showTools,
|
||||
}) {
|
||||
const [nodeHelp, setNodeHelp] = useState();
|
||||
const [zoomPercentage, setZoomPercentage] = useState(100);
|
||||
const svgRef = useRef(null);
|
||||
const gRef = useRef(null);
|
||||
|
||||
@ -20,6 +33,75 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
||||
'transform',
|
||||
`translate(${translation}) scale(${d3.event.transform.k})`
|
||||
);
|
||||
|
||||
setZoomPercentage(d3.event.transform.k * 100);
|
||||
};
|
||||
|
||||
const handlePan = direction => {
|
||||
const transform = d3.zoomTransform(d3.select(svgRef.current).node());
|
||||
|
||||
let { x: xPos, y: yPos } = transform;
|
||||
const { k: currentScale } = transform;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
yPos -= 50;
|
||||
break;
|
||||
case 'down':
|
||||
yPos += 50;
|
||||
break;
|
||||
case 'left':
|
||||
xPos -= 50;
|
||||
break;
|
||||
case 'right':
|
||||
xPos += 50;
|
||||
break;
|
||||
default:
|
||||
// Throw an error?
|
||||
break;
|
||||
}
|
||||
|
||||
d3.select(svgRef.current).call(
|
||||
zoomRef.transform,
|
||||
d3.zoomIdentity.translate(xPos, yPos).scale(currentScale)
|
||||
);
|
||||
};
|
||||
|
||||
const handlePanToMiddle = () => {
|
||||
const svgElement = document.getElementById('workflow-svg');
|
||||
const svgBoundingClientRect = svgElement.getBoundingClientRect();
|
||||
d3.select(svgRef.current).call(
|
||||
zoomRef.transform,
|
||||
d3.zoomIdentity
|
||||
.translate(0, svgBoundingClientRect.height / 2 - 30)
|
||||
.scale(1)
|
||||
);
|
||||
|
||||
setZoomPercentage(100);
|
||||
};
|
||||
|
||||
const handleZoomChange = newScale => {
|
||||
const [translateX, translateY] = getZoomTranslate(svgRef.current, newScale);
|
||||
|
||||
d3.select(svgRef.current).call(
|
||||
zoomRef.transform,
|
||||
d3.zoomIdentity.translate(translateX, translateY).scale(newScale)
|
||||
);
|
||||
setZoomPercentage(newScale * 100);
|
||||
};
|
||||
|
||||
const handleFitGraph = () => {
|
||||
const [scaleToFit, yTranslate] = calcZoomAndFit(
|
||||
gRef.current,
|
||||
svgRef.current
|
||||
);
|
||||
|
||||
d3.select(svgRef.current).call(
|
||||
zoomRef.transform,
|
||||
d3.zoomIdentity.translate(0, yTranslate).scale(scaleToFit)
|
||||
);
|
||||
|
||||
setZoomPercentage(scaleToFit * 100);
|
||||
};
|
||||
|
||||
const zoomRef = d3
|
||||
@ -34,12 +116,17 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
||||
|
||||
// Attempt to zoom the graph to fit the available screen space
|
||||
useEffect(() => {
|
||||
const [scaleToFit, yTranslate] = calcZoomAndFit(gRef.current);
|
||||
const [scaleToFit, yTranslate] = calcZoomAndFit(
|
||||
gRef.current,
|
||||
svgRef.current
|
||||
);
|
||||
|
||||
d3.select(svgRef.current).call(
|
||||
zoomRef.transform,
|
||||
d3.zoomIdentity.translate(0, yTranslate).scale(scaleToFit)
|
||||
);
|
||||
|
||||
setZoomPercentage(scaleToFit * 100);
|
||||
// We only want this to run once (when the component mounts)
|
||||
// Including zoomRef.transform in the deps array will cause this to
|
||||
// run very frequently.
|
||||
@ -78,10 +165,10 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
||||
return (
|
||||
<WorkflowOutputNode
|
||||
key={`node-${node.id}`}
|
||||
node={node}
|
||||
nodePositions={nodePositions}
|
||||
mouseEnter={() => setNodeHelp(node)}
|
||||
mouseLeave={() => setNodeHelp(null)}
|
||||
node={node}
|
||||
nodePositions={nodePositions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -90,8 +177,28 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
||||
]}
|
||||
</g>
|
||||
</svg>
|
||||
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
||||
{showTools && (
|
||||
<WorkflowTools
|
||||
onFitGraph={handleFitGraph}
|
||||
onPan={handlePan}
|
||||
onPanToMiddle={handlePanToMiddle}
|
||||
onZoomChange={handleZoomChange}
|
||||
zoomPercentage={zoomPercentage}
|
||||
/>
|
||||
)}
|
||||
{showKey && <WorkflowKey />}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowOutputGraph.propTypes = {
|
||||
links: arrayOf(shape()).isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
nodes: arrayOf(shape()).isRequired,
|
||||
showKey: bool.isRequired,
|
||||
showTools: bool.isRequired,
|
||||
};
|
||||
|
||||
export default WorkflowOutputGraph;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { shape } from 'prop-types';
|
||||
import { generateLine, getLinePoints } from '@util/workflow';
|
||||
|
||||
function WorkflowOutputLink({ link, nodePositions }) {
|
||||
@ -6,16 +7,16 @@ function WorkflowOutputLink({ link, nodePositions }) {
|
||||
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
||||
|
||||
useEffect(() => {
|
||||
if (link.edgeType === 'failure') {
|
||||
if (link.linkType === 'failure') {
|
||||
setPathStroke('#d9534f');
|
||||
}
|
||||
if (link.edgeType === 'success') {
|
||||
if (link.linkType === 'success') {
|
||||
setPathStroke('#5cb85c');
|
||||
}
|
||||
if (link.edgeType === 'always') {
|
||||
if (link.linkType === 'always') {
|
||||
setPathStroke('#337ab7');
|
||||
}
|
||||
}, [link.edgeType]);
|
||||
}, [link.linkType]);
|
||||
|
||||
useEffect(() => {
|
||||
const linePoints = getLinePoints(link, nodePositions);
|
||||
@ -37,4 +38,9 @@ function WorkflowOutputLink({ link, nodePositions }) {
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowOutputLink.propTypes = {
|
||||
link: shape().isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
};
|
||||
|
||||
export default WorkflowOutputLink;
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { StatusIcon } from '@components/Sparkline';
|
||||
import { WorkflowNodeTypeLetter } from '@components/Workflow';
|
||||
import { secondsToHHMMSS } from '@util/dates';
|
||||
import { JOB_TYPE_URL_SEGMENTS } from '@constants';
|
||||
import { constants as wfConstants } from '@util/workflow';
|
||||
|
||||
const NodeG = styled.g`
|
||||
@ -13,12 +14,12 @@ const NodeG = styled.g`
|
||||
`;
|
||||
|
||||
const JobTopLine = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
p {
|
||||
margin-left: 10px;
|
||||
@ -29,8 +30,8 @@ const JobTopLine = styled.div`
|
||||
`;
|
||||
|
||||
const Elapsed = styled.div`
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
@ -48,18 +49,19 @@ const NodeContents = styled.foreignObject`
|
||||
|
||||
const NodeDefaultLabel = styled.p`
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
function WorkflowOutputNode({
|
||||
node,
|
||||
nodePositions,
|
||||
history,
|
||||
i18n,
|
||||
mouseEnter,
|
||||
mouseLeave,
|
||||
i18n,
|
||||
node,
|
||||
nodePositions,
|
||||
}) {
|
||||
let borderColor = '#93969A';
|
||||
|
||||
@ -78,10 +80,7 @@ function WorkflowOutputNode({
|
||||
|
||||
const handleNodeClick = () => {
|
||||
if (node.job) {
|
||||
window.open(
|
||||
`/#/jobs/${JOB_TYPE_URL_SEGMENTS[node.job.type]}/${node.job.id}`,
|
||||
'_blank'
|
||||
);
|
||||
history.push(`/jobs/${node.job.id}/details`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,13 +95,13 @@ function WorkflowOutputNode({
|
||||
onMouseLeave={mouseLeave}
|
||||
>
|
||||
<rect
|
||||
width={wfConstants.nodeW}
|
||||
fill="#FFFFFF"
|
||||
height={wfConstants.nodeH}
|
||||
rx="2"
|
||||
ry="2"
|
||||
stroke={borderColor}
|
||||
strokeWidth="2px"
|
||||
fill="#FFFFFF"
|
||||
width={wfConstants.nodeW}
|
||||
/>
|
||||
<NodeContents height="60" width="180">
|
||||
{node.job ? (
|
||||
@ -133,4 +132,11 @@ function WorkflowOutputNode({
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(WorkflowOutputNode);
|
||||
WorkflowOutputNode.propTypes = {
|
||||
mouseEnter: func.isRequired,
|
||||
mouseLeave: func.isRequired,
|
||||
node: shape().isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(WorkflowOutputNode));
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import React from 'react';
|
||||
import { shape } from 'prop-types';
|
||||
import { constants as wfConstants } from '@util/workflow';
|
||||
|
||||
function WorkflowOutputStartNode({ nodePositions }) {
|
||||
return (
|
||||
<g id="node-1" transform={`translate(${nodePositions[1].x},0)`}>
|
||||
<rect
|
||||
width={wfConstants.rootW}
|
||||
fill="#0279BC"
|
||||
height={wfConstants.rootH}
|
||||
y="10"
|
||||
rx="2"
|
||||
ry="2"
|
||||
fill="#0279BC"
|
||||
width={wfConstants.rootW}
|
||||
y="10"
|
||||
/>
|
||||
{/* TODO: Translate this...? */}
|
||||
{/* TODO: We need to be able to handle translated text here */}
|
||||
<text x="13" y="30" dy=".35em" fill="white">
|
||||
START
|
||||
</text>
|
||||
@ -20,4 +21,8 @@ function WorkflowOutputStartNode({ nodePositions }) {
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowOutputStartNode.propTypes = {
|
||||
nodePositions: shape().isRequired,
|
||||
};
|
||||
|
||||
export default WorkflowOutputStartNode;
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { arrayOf, bool, func, shape } from 'prop-types';
|
||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||
import { CompassIcon, WrenchIcon } from '@patternfly/react-icons';
|
||||
import { StatusIcon } from '@components/Sparkline';
|
||||
import VerticalSeparator from '@components/VerticalSeparator';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Badge = styled(PFBadge)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const ActionButton = styled(Button)`
|
||||
border: none;
|
||||
margin: 0px 6px;
|
||||
padding: 6px 10px;
|
||||
&:hover {
|
||||
background-color: #0066cc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.pf-m-active {
|
||||
background-color: #0066cc;
|
||||
color: white;
|
||||
}
|
||||
`;
|
||||
|
||||
function WorkflowOutputToolbar({
|
||||
i18n,
|
||||
job,
|
||||
keyShown,
|
||||
nodes,
|
||||
onKeyToggle,
|
||||
onToolsToggle,
|
||||
toolsShown,
|
||||
}) {
|
||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||
|
||||
return (
|
||||
<div css="border-bottom: 1px solid grey; height: 56px; display: flex; alignItems: center">
|
||||
<div css="display: flex; align-items: center;">
|
||||
<StatusIcon status={job.status} css="margin-right: 20px" />
|
||||
<b>{job.name}</b>
|
||||
</div>
|
||||
<div css="display: flex; flex: 1; justify-content: flex-end; align-items: center;">
|
||||
<div>{i18n._(t`Total Nodes`)}</div>
|
||||
<Badge isRead>{totalNodes}</Badge>
|
||||
<VerticalSeparator />
|
||||
<Tooltip content={i18n._(t`Toggle Key`)} position="bottom">
|
||||
<ActionButton
|
||||
isActive={keyShown}
|
||||
onClick={onKeyToggle}
|
||||
variant="plain"
|
||||
>
|
||||
<CompassIcon />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
||||
<ActionButton
|
||||
isActive={toolsShown}
|
||||
onClick={onToolsToggle}
|
||||
variant="plain"
|
||||
>
|
||||
<WrenchIcon />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowOutputToolbar.propTypes = {
|
||||
job: shape().isRequired,
|
||||
keyShown: bool.isRequired,
|
||||
nodes: arrayOf(shape()),
|
||||
onKeyToggle: func.isRequired,
|
||||
onToolsToggle: func.isRequired,
|
||||
toolsShown: bool.isRequired,
|
||||
};
|
||||
|
||||
WorkflowOutputToolbar.defaultProps = {
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
export default withI18n()(WorkflowOutputToolbar);
|
||||
@ -3,3 +3,4 @@ export { default as WorkflowOutputGraph } from './WorkflowOutputGraph';
|
||||
export { default as WorkflowOutputLink } from './WorkflowOutputLink';
|
||||
export { default as WorkflowOutputNode } from './WorkflowOutputNode';
|
||||
export { default as WorkflowOutputStartNode } from './WorkflowOutputStartNode';
|
||||
export { default as WorkflowOutputToolbar } from './WorkflowOutputToolbar';
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
|
||||
import Templates from './Templates';
|
||||
|
||||
@ -2,15 +2,12 @@ import React from 'react';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func } from 'prop-types';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
|
||||
function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
||||
return (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
title={i18n._(t`Remove All Nodes`)}
|
||||
isOpen={true}
|
||||
onClose={onCancel}
|
||||
actions={[
|
||||
<Button
|
||||
key="remove"
|
||||
@ -29,6 +26,10 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
]}
|
||||
isOpen
|
||||
onClose={onCancel}
|
||||
title={i18n._(t`Remove All Nodes`)}
|
||||
variant="danger"
|
||||
>
|
||||
<p>
|
||||
{i18n._(
|
||||
@ -39,4 +40,9 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
||||
);
|
||||
}
|
||||
|
||||
DeleteAllNodesModal.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
onConfirm: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(DeleteAllNodesModal);
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
|
||||
function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
||||
@ -13,18 +14,18 @@ function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
||||
onClose={onCancel}
|
||||
actions={[
|
||||
<Button
|
||||
key="remove"
|
||||
variant="danger"
|
||||
aria-label={i18n._(t`Confirm link removal`)}
|
||||
key="remove"
|
||||
onClick={() => onConfirm()}
|
||||
variant="danger"
|
||||
>
|
||||
{i18n._(t`Remove`)}
|
||||
</Button>,
|
||||
<Button
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Cancel link removal`)}
|
||||
key="cancel"
|
||||
onClick={onCancel}
|
||||
variant="secondary"
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>,
|
||||
@ -45,4 +46,10 @@ function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
||||
);
|
||||
}
|
||||
|
||||
LinkDeleteModal.propTypes = {
|
||||
linkToDelete: shape().isRequired,
|
||||
onCancel: func.isRequired,
|
||||
onConfirm: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(LinkDeleteModal);
|
||||
|
||||
@ -1,23 +1,17 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Modal } from '@patternfly/react-core';
|
||||
import { Button, FormGroup, Modal } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import { func, node, string } from 'prop-types';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
|
||||
function LinkModal({
|
||||
i18n,
|
||||
header,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
edgeType = 'success',
|
||||
}) {
|
||||
const [newEdgeType, setNewEdgeType] = useState(edgeType);
|
||||
function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
||||
const [newLinkType, setNewLinkType] = useState(linkType);
|
||||
return (
|
||||
<Modal
|
||||
width={600}
|
||||
header={header}
|
||||
isOpen={true}
|
||||
isOpen
|
||||
title={i18n._(t`Workflow Link`)}
|
||||
onClose={onCancel}
|
||||
actions={[
|
||||
@ -25,7 +19,7 @@ function LinkModal({
|
||||
key="save"
|
||||
variant="primary"
|
||||
aria-label={i18n._(t`Save link changes`)}
|
||||
onClick={() => onConfirm(newEdgeType)}
|
||||
onClick={() => onConfirm(newLinkType)}
|
||||
>
|
||||
{i18n._(t`Save`)}
|
||||
</Button>,
|
||||
@ -42,7 +36,7 @@ function LinkModal({
|
||||
<FormGroup fieldId="link-select" label={i18n._(t`Run`)}>
|
||||
<AnsibleSelect
|
||||
id="link-select"
|
||||
value={newEdgeType}
|
||||
value={newLinkType}
|
||||
data={[
|
||||
{
|
||||
value: 'always',
|
||||
@ -61,7 +55,7 @@ function LinkModal({
|
||||
},
|
||||
]}
|
||||
onChange={(event, value) => {
|
||||
setNewEdgeType(value);
|
||||
setNewLinkType(value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
@ -69,4 +63,15 @@ function LinkModal({
|
||||
);
|
||||
}
|
||||
|
||||
LinkModal.propTypes = {
|
||||
linkType: string,
|
||||
header: node.isRequired,
|
||||
onCancel: func.isRequired,
|
||||
onConfirm: func.isRequired,
|
||||
};
|
||||
|
||||
LinkModal.defaultProps = {
|
||||
linkType: 'success',
|
||||
};
|
||||
|
||||
export default withI18n()(LinkModal);
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
|
||||
function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) {
|
||||
@ -45,4 +46,10 @@ function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) {
|
||||
);
|
||||
}
|
||||
|
||||
NodeDeleteModal.propTypes = {
|
||||
nodeToDelete: shape().isRequired,
|
||||
onCancel: func.isRequired,
|
||||
onConfirm: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(NodeDeleteModal);
|
||||
|
||||
@ -2,82 +2,84 @@ import React, { useState } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { bool, func, node, shape } from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
WizardContextConsumer,
|
||||
WizardFooter,
|
||||
} from '@patternfly/react-core';
|
||||
import NodeTypeStep from './NodeTypeStep/NodeTypeStep';
|
||||
import RunStep from './RunStep';
|
||||
import NodeNextButton from './NodeNextButton';
|
||||
import { Wizard } from '@components/Wizard';
|
||||
import Wizard from '@components/Wizard';
|
||||
import { NodeTypeStep } from './NodeTypeStep';
|
||||
import { RunStep, NodeNextButton } from '.';
|
||||
|
||||
function NodeModal({
|
||||
askLinkType,
|
||||
history,
|
||||
i18n,
|
||||
title,
|
||||
nodeToEdit,
|
||||
onClose,
|
||||
onSave,
|
||||
node,
|
||||
askLinkType,
|
||||
title,
|
||||
}) {
|
||||
let defaultNodeType = 'job_template';
|
||||
let defaultNodeResource = null;
|
||||
let defaultApprovalName = '';
|
||||
let defaultApprovalDescription = '';
|
||||
let defaultApprovalName = '';
|
||||
let defaultApprovalTimeout = 0;
|
||||
if (node && node.unifiedJobTemplate) {
|
||||
let defaultNodeResource = null;
|
||||
let defaultNodeType = 'job_template';
|
||||
if (nodeToEdit && nodeToEdit.unifiedJobTemplate) {
|
||||
if (
|
||||
node &&
|
||||
node.unifiedJobTemplate &&
|
||||
(node.unifiedJobTemplate.type || node.unifiedJobTemplate.unified_job_type)
|
||||
nodeToEdit &&
|
||||
nodeToEdit.unifiedJobTemplate &&
|
||||
(nodeToEdit.unifiedJobTemplate.type ||
|
||||
nodeToEdit.unifiedJobTemplate.unified_job_type)
|
||||
) {
|
||||
const ujtType =
|
||||
node.unifiedJobTemplate.type ||
|
||||
node.unifiedJobTemplate.unified_job_type;
|
||||
nodeToEdit.unifiedJobTemplate.type ||
|
||||
nodeToEdit.unifiedJobTemplate.unified_job_type;
|
||||
switch (ujtType) {
|
||||
case 'job_template':
|
||||
case 'job':
|
||||
defaultNodeType = 'job_template';
|
||||
defaultNodeResource = node.unifiedJobTemplate;
|
||||
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
||||
break;
|
||||
case 'project':
|
||||
case 'project_update':
|
||||
defaultNodeType = 'project_sync';
|
||||
defaultNodeResource = node.unifiedJobTemplate;
|
||||
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
||||
break;
|
||||
case 'inventory_source':
|
||||
case 'inventory_update':
|
||||
defaultNodeType = 'inventory_source_sync';
|
||||
defaultNodeResource = node.unifiedJobTemplate;
|
||||
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
||||
break;
|
||||
case 'workflow_job_template':
|
||||
case 'workflow_job':
|
||||
defaultNodeType = 'workflow_job_template';
|
||||
defaultNodeResource = node.unifiedJobTemplate;
|
||||
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
||||
break;
|
||||
case 'workflow_approval_template':
|
||||
case 'workflow_approval':
|
||||
defaultNodeType = 'approval';
|
||||
defaultApprovalName = node.unifiedJobTemplate.name;
|
||||
defaultApprovalDescription = node.unifiedJobTemplate.description;
|
||||
defaultApprovalTimeout = node.unifiedJobTemplate.timeout;
|
||||
defaultApprovalName = nodeToEdit.unifiedJobTemplate.name;
|
||||
defaultApprovalDescription =
|
||||
nodeToEdit.unifiedJobTemplate.description;
|
||||
defaultApprovalTimeout = nodeToEdit.unifiedJobTemplate.timeout;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
const [nodeType, setNodeType] = useState(defaultNodeType);
|
||||
const [linkType, setLinkType] = useState('success');
|
||||
const [nodeResource, setNodeResource] = useState(defaultNodeResource);
|
||||
const [triggerNext, setTriggerNext] = useState(0);
|
||||
const [approvalName, setApprovalName] = useState(defaultApprovalName);
|
||||
const [approvalDescription, setApprovalDescription] = useState(
|
||||
defaultApprovalDescription
|
||||
);
|
||||
const [approvalName, setApprovalName] = useState(defaultApprovalName);
|
||||
const [approvalTimeout, setApprovalTimeout] = useState(
|
||||
defaultApprovalTimeout
|
||||
);
|
||||
const [linkType, setLinkType] = useState('success');
|
||||
const [nodeResource, setNodeResource] = useState(defaultNodeResource);
|
||||
const [nodeType, setNodeType] = useState(defaultNodeType);
|
||||
const [triggerNext, setTriggerNext] = useState(0);
|
||||
|
||||
const clearQueryParams = () => {
|
||||
const parts = history.location.search.replace(/^\?/, '').split('&');
|
||||
@ -95,19 +97,17 @@ function NodeModal({
|
||||
const resource =
|
||||
nodeType === 'approval'
|
||||
? {
|
||||
name: approvalName,
|
||||
description: approvalDescription,
|
||||
name: approvalName,
|
||||
timeout: approvalTimeout,
|
||||
type: 'workflow_approval_template',
|
||||
}
|
||||
: nodeResource;
|
||||
|
||||
// TODO: pick edgeType or linkType and be consistent across all files.
|
||||
|
||||
onSave({
|
||||
nodeType,
|
||||
edgeType: linkType,
|
||||
linkType,
|
||||
nodeResource: resource,
|
||||
nodeType,
|
||||
});
|
||||
};
|
||||
|
||||
@ -145,15 +145,15 @@ function NodeModal({
|
||||
(nodeType === 'approval' && approvalName !== ''),
|
||||
component: (
|
||||
<NodeTypeStep
|
||||
nodeType={nodeType}
|
||||
updateNodeType={handleNodeTypeChange}
|
||||
nodeResource={nodeResource}
|
||||
updateNodeResource={setNodeResource}
|
||||
name={approvalName}
|
||||
updateName={setApprovalName}
|
||||
description={approvalDescription}
|
||||
updateDescription={setApprovalDescription}
|
||||
name={approvalName}
|
||||
nodeResource={nodeResource}
|
||||
nodeType={nodeType}
|
||||
timeout={approvalTimeout}
|
||||
updateDescription={setApprovalDescription}
|
||||
updateName={setApprovalName}
|
||||
updateNodeResource={setNodeResource}
|
||||
updateNodeType={handleNodeTypeChange}
|
||||
updateTimeout={setApprovalTimeout}
|
||||
/>
|
||||
),
|
||||
@ -198,15 +198,27 @@ function NodeModal({
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
style={{ overflow: 'scroll' }}
|
||||
footer={CustomFooter}
|
||||
isOpen
|
||||
steps={steps}
|
||||
title={wizardTitle}
|
||||
onClose={handleCancel}
|
||||
onSave={handleSaveNode}
|
||||
footer={CustomFooter}
|
||||
steps={steps}
|
||||
css="overflow: scroll"
|
||||
title={wizardTitle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
NodeModal.propTypes = {
|
||||
askLinkType: bool.isRequired,
|
||||
nodeToEdit: shape(),
|
||||
onClose: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
title: node.isRequired,
|
||||
};
|
||||
|
||||
NodeModal.defaultProps = {
|
||||
nodeToEdit: null,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(NodeModal));
|
||||
|
||||
@ -1,22 +1,20 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, number, shape, string } from 'prop-types';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
|
||||
function NodeNextButton({
|
||||
i18n,
|
||||
activeStep,
|
||||
buttonText,
|
||||
onClick,
|
||||
onNext,
|
||||
triggerNext,
|
||||
onClick,
|
||||
buttonText,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!triggerNext) {
|
||||
return;
|
||||
}
|
||||
onNext();
|
||||
}, [triggerNext]);
|
||||
}, [onNext, triggerNext]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
@ -30,4 +28,12 @@ function NodeNextButton({
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(NodeNextButton);
|
||||
NodeNextButton.propTypes = {
|
||||
activeStep: shape().isRequired,
|
||||
buttonText: string.isRequired,
|
||||
onClick: func.isRequired,
|
||||
onNext: func.isRequired,
|
||||
triggerNext: number.isRequired,
|
||||
};
|
||||
|
||||
export default NodeNextButton;
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { InventorySourcesAPI } from '@api';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import PaginatedDataList from '@components/PaginatedDataList';
|
||||
@ -15,14 +16,14 @@ const QS_CONFIG = getQSConfig('inventory_sources', {
|
||||
});
|
||||
|
||||
function InventorySourcesList({
|
||||
i18n,
|
||||
history,
|
||||
i18n,
|
||||
nodeResource,
|
||||
updateNodeResource,
|
||||
}) {
|
||||
const [inventorySources, setInventorySources] = useState([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
const [inventorySources, setInventorySources] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@ -47,9 +48,24 @@ function InventorySourcesList({
|
||||
<PaginatedDataList
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
items={inventorySources}
|
||||
itemCount={count}
|
||||
items={inventorySources}
|
||||
onRowClick={row => updateNodeResource(row)}
|
||||
qsConfig={QS_CONFIG}
|
||||
showPageSizeOptions={false}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={!!(nodeResource && nodeResource.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => updateNodeResource(item)}
|
||||
onDeselect={() => updateNodeResource(null)}
|
||||
isRadio
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
toolbarColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
@ -58,24 +74,17 @@ function InventorySourcesList({
|
||||
isSearchable: true,
|
||||
},
|
||||
]}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={
|
||||
nodeResource && nodeResource.id === item.id ? true : false
|
||||
}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => updateNodeResource(item)}
|
||||
onDeselect={() => updateNodeResource(null)}
|
||||
isRadio={true}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
InventorySourcesList.propTypes = {
|
||||
nodeResource: shape(),
|
||||
updateNodeResource: func.isRequired,
|
||||
};
|
||||
|
||||
InventorySourcesList.defaultProps = {
|
||||
nodeResource: null,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(InventorySourcesList));
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { JobTemplatesAPI } from '@api';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import PaginatedDataList from '@components/PaginatedDataList';
|
||||
@ -15,10 +16,10 @@ const QS_CONFIG = getQSConfig('job_templates', {
|
||||
});
|
||||
|
||||
function JobTemplatesList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||
const [jobTemplates, setJobTemplates] = useState([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [jobTemplates, setJobTemplates] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -44,9 +45,24 @@ function JobTemplatesList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||
<PaginatedDataList
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
items={jobTemplates}
|
||||
itemCount={count}
|
||||
items={jobTemplates}
|
||||
onRowClick={row => updateNodeResource(row)}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={!!(nodeResource && nodeResource.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => updateNodeResource(item)}
|
||||
onDeselect={() => updateNodeResource(null)}
|
||||
isRadio
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
toolbarColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
@ -55,24 +71,17 @@ function JobTemplatesList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||
isSearchable: true,
|
||||
},
|
||||
]}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={
|
||||
nodeResource && nodeResource.id === item.id ? true : false
|
||||
}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => updateNodeResource(item)}
|
||||
onDeselect={() => updateNodeResource(null)}
|
||||
isRadio={true}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
JobTemplatesList.propTypes = {
|
||||
nodeResource: shape(),
|
||||
updateNodeResource: func.isRequired,
|
||||
};
|
||||
|
||||
JobTemplatesList.defaultProps = {
|
||||
nodeResource: null,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(JobTemplatesList));
|
||||
|
||||
@ -1,19 +1,25 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, number, shape, string } from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Formik, Field } from 'formik';
|
||||
import { Form, FormGroup, TextInput } from '@patternfly/react-core';
|
||||
import { Divider } from '@patternfly/react-core/dist/esm/experimental';
|
||||
import FormRow from '@components/FormRow';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import VerticalSeperator from '@components/VerticalSeparator';
|
||||
|
||||
import InventorySourcesList from './InventorySourcesList';
|
||||
import JobTemplatesList from './JobTemplatesList';
|
||||
import ProjectsList from './ProjectsList';
|
||||
import WorkflowJobTemplatesList from './WorkflowJobTemplatesList';
|
||||
|
||||
const Divider = styled.div`
|
||||
height: 1px;
|
||||
background-color: var(--pf-global--Color--light-300);
|
||||
border: 0;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const TimeoutInput = styled(TextInput)`
|
||||
width: 200px;
|
||||
:not(:first-of-type) {
|
||||
@ -26,16 +32,16 @@ const TimeoutLabel = styled.p`
|
||||
`;
|
||||
|
||||
function NodeTypeStep({
|
||||
i18n,
|
||||
nodeType = 'job_template',
|
||||
updateNodeType,
|
||||
nodeResource,
|
||||
updateNodeResource,
|
||||
name,
|
||||
updateName,
|
||||
description,
|
||||
i18n,
|
||||
name,
|
||||
nodeResource,
|
||||
nodeType,
|
||||
timeout,
|
||||
updateDescription,
|
||||
timeout = 0,
|
||||
updateName,
|
||||
updateNodeResource,
|
||||
updateNodeType,
|
||||
updateTimeout,
|
||||
}) {
|
||||
return (
|
||||
@ -132,21 +138,21 @@ function NodeTypeStep({
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="approval-name"
|
||||
isRequired={true}
|
||||
isRequired
|
||||
isValid={isValid}
|
||||
label={i18n._(t`Name`)}
|
||||
>
|
||||
<TextInput
|
||||
autoFocus
|
||||
id="approval-name"
|
||||
isRequired={true}
|
||||
isRequired
|
||||
isValid={isValid}
|
||||
type="text"
|
||||
{...field}
|
||||
onChange={(value, event) => {
|
||||
onChange={(value, evt) => {
|
||||
updateName(value);
|
||||
field.onChange(event);
|
||||
field.onChange(evt);
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
@ -165,9 +171,9 @@ function NodeTypeStep({
|
||||
id="approval-description"
|
||||
type="text"
|
||||
{...field}
|
||||
onChange={value => {
|
||||
onChange={(value, evt) => {
|
||||
updateDescription(value);
|
||||
field.onChange(event);
|
||||
field.onChange(evt);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
@ -190,7 +196,7 @@ function NodeTypeStep({
|
||||
min="0"
|
||||
step="1"
|
||||
{...field}
|
||||
onChange={value => {
|
||||
onChange={(value, evt) => {
|
||||
if (!value || value === '') {
|
||||
value = 0;
|
||||
}
|
||||
@ -198,7 +204,7 @@ function NodeTypeStep({
|
||||
Number(value) * 60 +
|
||||
Number(form.values.timeoutSeconds)
|
||||
);
|
||||
field.onChange(event);
|
||||
field.onChange(evt);
|
||||
}}
|
||||
/>
|
||||
<TimeoutLabel>min</TimeoutLabel>
|
||||
@ -215,7 +221,7 @@ function NodeTypeStep({
|
||||
min="0"
|
||||
step="1"
|
||||
{...field}
|
||||
onChange={value => {
|
||||
onChange={(value, evt) => {
|
||||
if (!value || value === '') {
|
||||
value = 0;
|
||||
}
|
||||
@ -223,7 +229,7 @@ function NodeTypeStep({
|
||||
Number(value) +
|
||||
Number(form.values.timeoutMinutes) * 60
|
||||
);
|
||||
field.onChange(event);
|
||||
field.onChange(evt);
|
||||
}}
|
||||
/>
|
||||
<TimeoutLabel>sec</TimeoutLabel>
|
||||
@ -241,4 +247,25 @@ function NodeTypeStep({
|
||||
);
|
||||
}
|
||||
|
||||
NodeTypeStep.propTypes = {
|
||||
description: string,
|
||||
name: string,
|
||||
nodeResource: shape(),
|
||||
nodeType: string,
|
||||
timeout: number,
|
||||
updateDescription: func.isRequired,
|
||||
updateName: func.isRequired,
|
||||
updateNodeResource: func.isRequired,
|
||||
updateNodeType: func.isRequired,
|
||||
updateTimeout: func.isRequired,
|
||||
};
|
||||
|
||||
NodeTypeStep.defaultProps = {
|
||||
description: '',
|
||||
name: '',
|
||||
nodeResource: null,
|
||||
nodeType: 'job_template',
|
||||
timeout: 0,
|
||||
};
|
||||
|
||||
export default withI18n()(NodeTypeStep);
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { ProjectsAPI } from '@api';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import PaginatedDataList from '@components/PaginatedDataList';
|
||||
@ -14,11 +15,11 @@ const QS_CONFIG = getQSConfig('projects', {
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function ProjectsList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||
const [projects, setProjects] = useState([]);
|
||||
function ProjectsList({ history, i18n, nodeResource, updateNodeResource }) {
|
||||
const [count, setCount] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [projects, setProjects] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -42,9 +43,24 @@ function ProjectsList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||
<PaginatedDataList
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
items={projects}
|
||||
itemCount={count}
|
||||
items={projects}
|
||||
onRowClick={row => updateNodeResource(row)}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={!!(nodeResource && nodeResource.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => updateNodeResource(item)}
|
||||
onDeselect={() => updateNodeResource(null)}
|
||||
isRadio
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
toolbarColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
@ -53,24 +69,17 @@ function ProjectsList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||
isSearchable: true,
|
||||
},
|
||||
]}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={
|
||||
nodeResource && nodeResource.id === item.id ? true : false
|
||||
}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => updateNodeResource(item)}
|
||||
onDeselect={() => updateNodeResource(null)}
|
||||
isRadio={true}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectsList.propTypes = {
|
||||
nodeResource: shape(),
|
||||
updateNodeResource: func.isRequired,
|
||||
};
|
||||
|
||||
ProjectsList.defaultProps = {
|
||||
nodeResource: null,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(ProjectsList));
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { WorkflowJobTemplatesAPI } from '@api';
|
||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||
import PaginatedDataList from '@components/PaginatedDataList';
|
||||
@ -15,15 +16,15 @@ const QS_CONFIG = getQSConfig('workflow_job_templates', {
|
||||
});
|
||||
|
||||
function WorkflowJobTemplatesList({
|
||||
i18n,
|
||||
history,
|
||||
i18n,
|
||||
nodeResource,
|
||||
updateNodeResource,
|
||||
}) {
|
||||
const [workflowJobTemplates, setWorkflowJobTemplates] = useState([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [workflowJobTemplates, setWorkflowJobTemplates] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -49,9 +50,24 @@ function WorkflowJobTemplatesList({
|
||||
<PaginatedDataList
|
||||
contentError={error}
|
||||
hasContentLoading={isLoading}
|
||||
items={workflowJobTemplates}
|
||||
itemCount={count}
|
||||
items={workflowJobTemplates}
|
||||
onRowClick={row => updateNodeResource(row)}
|
||||
qsConfig={QS_CONFIG}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={!!(nodeResource && nodeResource.id === item.id)}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => updateNodeResource(item)}
|
||||
onDeselect={() => updateNodeResource(null)}
|
||||
isRadio
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
toolbarColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
@ -60,24 +76,17 @@ function WorkflowJobTemplatesList({
|
||||
isSearchable: true,
|
||||
},
|
||||
]}
|
||||
renderItem={item => (
|
||||
<CheckboxListItem
|
||||
isSelected={
|
||||
nodeResource && nodeResource.id === item.id ? true : false
|
||||
}
|
||||
itemId={item.id}
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
label={item.name}
|
||||
onSelect={() => updateNodeResource(item)}
|
||||
onDeselect={() => updateNodeResource(null)}
|
||||
isRadio={true}
|
||||
/>
|
||||
)}
|
||||
renderToolbar={props => <DataListToolbar {...props} fillWidth />}
|
||||
showPageSizeOptions={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
WorkflowJobTemplatesList.propTypes = {
|
||||
nodeResource: shape(),
|
||||
updateNodeResource: func.isRequired,
|
||||
};
|
||||
|
||||
WorkflowJobTemplatesList.defaultProps = {
|
||||
nodeResource: null,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(WorkflowJobTemplatesList));
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
export { default as InventorySourcesList } from './InventorySourcesList';
|
||||
export { default as JobTemplatesList } from './JobTemplatesList';
|
||||
export { default as NodeTypeStep } from './NodeTypeStep';
|
||||
export { default as ProjectsList } from './ProjectsList';
|
||||
export {
|
||||
default as WorkflowJobTemplatesList,
|
||||
} from './WorkflowJobTemplatesList';
|
||||
@ -2,17 +2,18 @@ import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { func, string } from 'prop-types';
|
||||
import { Title } from '@patternfly/react-core';
|
||||
import { SelectableCard } from '@components/SelectableCard';
|
||||
import SelectableCard from '@components/SelectableCard';
|
||||
|
||||
const Grid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 33% 33% 33%;
|
||||
grid-gap: 20px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-auto-rows: 100px;
|
||||
width: 100%;
|
||||
grid-gap: 20px;
|
||||
grid-template-columns: 33% 33% 33%;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
margin: 20px 0px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
function RunStep({ i18n, linkType, updateLinkType }) {
|
||||
@ -56,4 +57,9 @@ function RunStep({ i18n, linkType, updateLinkType }) {
|
||||
);
|
||||
}
|
||||
|
||||
RunStep.propTypes = {
|
||||
linkType: string.isRequired,
|
||||
updateLinkType: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(RunStep);
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export { default as NodeModal } from './NodeModal';
|
||||
export { default as NodeNextButton } from './NodeNextButton';
|
||||
export { default as RunStep } from './RunStep';
|
||||
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Modal } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
|
||||
function NodeViewModal({ i18n, onClose, node }) {
|
||||
return (
|
||||
<Modal
|
||||
isLarge
|
||||
isOpen
|
||||
title={i18n._(t`Node Details | ${node.unifiedJobTemplate.name}`)}
|
||||
onClose={onClose}
|
||||
>
|
||||
Coming soon :)
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
NodeViewModal.propTypes = {
|
||||
node: shape().isRequired,
|
||||
onClose: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(NodeViewModal);
|
||||
@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
|
||||
function ApprovalDetails({ i18n, node }) {
|
||||
const { name, description, timeout } = node.unifiedJobTemplate;
|
||||
|
||||
let timeoutValue = i18n._(t`None`);
|
||||
|
||||
if (timeout) {
|
||||
const minutes = Math.floor(timeout / 60);
|
||||
const seconds = timeout - minutes * 60;
|
||||
timeoutValue = i18n._(t`${minutes}min ${seconds}sec`);
|
||||
}
|
||||
return (
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Node Type`)} value={i18n._(t`Approval`)} />
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
<Detail label={i18n._(t`Timeout`)} value={timeoutValue} />
|
||||
</DetailList>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(ApprovalDetails);
|
||||
@ -1,165 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { InventorySourcesAPI } from '@api';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||
import { CredentialChip } from '@components/Chip';
|
||||
|
||||
function InventorySourceSyncDetails({ i18n, node }) {
|
||||
const [inventorySource, setInventorySource] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [noReadAccess, setNoReadAccess] = useState(false);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [optionsActions, setOptionsActions] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchInventorySource() {
|
||||
try {
|
||||
const [
|
||||
{ data },
|
||||
{
|
||||
data: { actions },
|
||||
},
|
||||
] = await Promise.all([
|
||||
InventorySourcesAPI.readDetail(node.unifiedJobTemplate.id),
|
||||
InventorySourcesAPI.readOptions(),
|
||||
]);
|
||||
setInventorySource(data);
|
||||
setOptionsActions(actions);
|
||||
} catch (err) {
|
||||
if (err.response.status === 403) {
|
||||
setNoReadAccess(true);
|
||||
} else {
|
||||
setContentError(err);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchInventorySource();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
|
||||
if (noReadAccess) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<Trans>
|
||||
Your account does not have read access to this inventory source so
|
||||
the displayed details will be limited.
|
||||
</Trans>
|
||||
</p>
|
||||
<br />
|
||||
<DetailList gutter="sm">
|
||||
<Detail
|
||||
label={i18n._(t`Node Type`)}
|
||||
value={i18n._(t`Inventory Source Sync`)}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Name`)}
|
||||
value={node.unifiedJobTemplate.name}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Description`)}
|
||||
value={node.unifiedJobTemplate.description}
|
||||
/>
|
||||
</DetailList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
custom_virtualenv,
|
||||
description,
|
||||
group_by,
|
||||
instance_filters,
|
||||
name,
|
||||
source,
|
||||
source_path,
|
||||
source_regions,
|
||||
source_script,
|
||||
source_vars,
|
||||
summary_fields,
|
||||
timeout,
|
||||
verbosity,
|
||||
} = inventorySource;
|
||||
|
||||
let sourceValue = '';
|
||||
let verbosityValue = '';
|
||||
|
||||
optionsActions.GET.source.choices.forEach(choice => {
|
||||
if (choice[0] === source) {
|
||||
sourceValue = choice[1];
|
||||
}
|
||||
});
|
||||
|
||||
optionsActions.GET.verbosity.choices.forEach(choice => {
|
||||
if (choice[0] === verbosity) {
|
||||
verbosityValue = choice[1];
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<DetailList gutter="sm">
|
||||
<Detail
|
||||
label={i18n._(t`Node Type`)}
|
||||
value={i18n._(t`Inventory Source Sync`)}
|
||||
/>
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
{summary_fields.inventory && (
|
||||
<Detail
|
||||
label={i18n._(t`Inventory`)}
|
||||
value={summary_fields.inventory.name}
|
||||
/>
|
||||
)}
|
||||
{summary_fields.credential && (
|
||||
<Detail
|
||||
label={i18n._(t`Credential`)}
|
||||
value={
|
||||
<CredentialChip
|
||||
key={summary_fields.credential.id}
|
||||
credential={summary_fields.credential}
|
||||
isReadOnly
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`Source`)} value={sourceValue} />
|
||||
<Detail label={i18n._(t`Source Path`)} value={source_path} />
|
||||
<Detail label={i18n._(t`Source Script`)} value={source_script} />
|
||||
{/* this should probably be tags built from OPTIONS*/}
|
||||
<Detail label={i18n._(t`Source Regions`)} value={source_regions} />
|
||||
<Detail label={i18n._(t`Instance Filters`)} value={instance_filters} />
|
||||
{/* this should probably be tags built from OPTIONS */}
|
||||
<Detail label={i18n._(t`Only Group By`)} value={group_by} />
|
||||
<Detail
|
||||
label={i18n._(t`Timeout`)}
|
||||
value={`${timeout} ${i18n._(t`Seconds`)}`}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Ansible Environment`)}
|
||||
value={custom_virtualenv}
|
||||
/>
|
||||
<Detail label={i18n._(t`Verbosity`)} value={verbosityValue} />
|
||||
<VariablesDetail
|
||||
label={i18n._(t`Variables`)}
|
||||
value={source_vars}
|
||||
rows={4}
|
||||
/>
|
||||
</DetailList>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(InventorySourceSyncDetails);
|
||||
@ -1,564 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import jsyaml from 'js-yaml';
|
||||
import styled from 'styled-components';
|
||||
import { JobTemplatesAPI, WorkflowJobTemplateNodesAPI } from '@api';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
||||
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||
|
||||
const Overridden = styled.div`
|
||||
color: var(--pf-global--warning-color--100);
|
||||
`;
|
||||
|
||||
function JobTemplateDetails({ i18n, node }) {
|
||||
const [jobTemplate, setJobTemplate] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [noReadAccess, setNoReadAccess] = useState(false);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [optionsActions, setOptionsActions] = useState(null);
|
||||
const [instanceGroups, setInstanceGroups] = useState([]);
|
||||
const [nodeCredentials, setNodeCredentials] = useState([]);
|
||||
const [launchConf, setLaunchConf] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchJobTemplate() {
|
||||
try {
|
||||
const [
|
||||
{ data },
|
||||
{
|
||||
data: { results: instanceGroups },
|
||||
},
|
||||
{ data: launchConf },
|
||||
{
|
||||
data: { actions },
|
||||
},
|
||||
{
|
||||
data: { results: nodeCredentials },
|
||||
},
|
||||
] = await Promise.all([
|
||||
JobTemplatesAPI.readDetail(node.unifiedJobTemplate.id),
|
||||
JobTemplatesAPI.readInstanceGroups(node.unifiedJobTemplate.id),
|
||||
JobTemplatesAPI.readLaunch(node.unifiedJobTemplate.id),
|
||||
JobTemplatesAPI.readOptions(),
|
||||
WorkflowJobTemplateNodesAPI.readCredentials(
|
||||
node.originalNodeObject.id
|
||||
),
|
||||
]);
|
||||
setJobTemplate(data);
|
||||
setInstanceGroups(instanceGroups);
|
||||
setLaunchConf(launchConf);
|
||||
setOptionsActions(actions);
|
||||
setNodeCredentials(nodeCredentials);
|
||||
} catch (err) {
|
||||
if (err.response.status === 403) {
|
||||
setNoReadAccess(true);
|
||||
} else {
|
||||
setContentError(err);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchJobTemplate();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
|
||||
if (noReadAccess) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<Trans>
|
||||
Your account does not have read access to this job template so the
|
||||
displayed details will be limited.
|
||||
</Trans>
|
||||
</p>
|
||||
<br />
|
||||
<DetailList gutter="sm">
|
||||
<Detail
|
||||
label={i18n._(t`Node Type`)}
|
||||
value={i18n._(t`Job Template`)}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Name`)}
|
||||
value={node.unifiedJobTemplate.name}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Description`)}
|
||||
value={node.unifiedJobTemplate.description}
|
||||
/>
|
||||
</DetailList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
job_type: nodeJobType,
|
||||
limit: nodeLimit,
|
||||
scm_branch: nodeScmBranch,
|
||||
inventory: nodeInventory,
|
||||
verbosity: nodeVerbosity,
|
||||
job_tags: nodeJobTags,
|
||||
skip_tags: nodeSkipTags,
|
||||
diff_mode: nodeDiffMode,
|
||||
extra_data: nodeExtraData,
|
||||
summary_fields: nodeSummaryFields,
|
||||
} = node.originalNodeObject;
|
||||
|
||||
let {
|
||||
ask_job_type_on_launch,
|
||||
ask_limit_on_launch,
|
||||
ask_scm_branch_on_launch,
|
||||
ask_inventory_on_launch,
|
||||
ask_verbosity_on_launch,
|
||||
ask_tags_on_launch,
|
||||
ask_skip_tags_on_launch,
|
||||
ask_diff_mode_on_launch,
|
||||
ask_credential_on_launch,
|
||||
ask_variables_on_launch,
|
||||
description,
|
||||
diff_mode,
|
||||
extra_vars,
|
||||
forks,
|
||||
host_config_key,
|
||||
job_slice_count,
|
||||
job_tags,
|
||||
job_type,
|
||||
name,
|
||||
limit,
|
||||
playbook,
|
||||
skip_tags,
|
||||
timeout,
|
||||
summary_fields,
|
||||
verbosity,
|
||||
scm_branch,
|
||||
inventory,
|
||||
} = jobTemplate;
|
||||
|
||||
const jobTypeOverridden =
|
||||
ask_job_type_on_launch && nodeJobType !== null && job_type !== nodeJobType;
|
||||
const limitOverridden =
|
||||
ask_limit_on_launch && nodeLimit !== null && limit !== nodeLimit;
|
||||
const scmBranchOverridden =
|
||||
ask_scm_branch_on_launch &&
|
||||
nodeScmBranch !== null &&
|
||||
scm_branch !== nodeScmBranch;
|
||||
const inventoryOverridden =
|
||||
ask_inventory_on_launch &&
|
||||
nodeInventory !== null &&
|
||||
inventory !== nodeInventory;
|
||||
const verbosityOverridden =
|
||||
ask_verbosity_on_launch &&
|
||||
nodeVerbosity !== null &&
|
||||
verbosity !== nodeVerbosity;
|
||||
const jobTagsOverridden =
|
||||
ask_tags_on_launch && nodeJobTags !== null && job_tags !== nodeJobTags;
|
||||
const skipTagsOverridden =
|
||||
ask_skip_tags_on_launch &&
|
||||
nodeSkipTags !== null &&
|
||||
skip_tags !== nodeSkipTags;
|
||||
const diffModeOverridden =
|
||||
ask_diff_mode_on_launch &&
|
||||
nodeDiffMode !== null &&
|
||||
diff_mode !== nodeDiffMode;
|
||||
const credentialOverridden =
|
||||
ask_credential_on_launch && nodeCredentials.length > 0;
|
||||
let variablesOverridden = false;
|
||||
let variablesToShow = extra_vars;
|
||||
|
||||
const deepObjectMatch = (obj1, obj2) => {
|
||||
if (obj1 === obj2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
obj1 === null ||
|
||||
obj2 === null ||
|
||||
typeof obj1 !== 'object' ||
|
||||
typeof obj2 !== 'object'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const obj1Keys = Object.keys(obj1);
|
||||
const obj2Keys = Object.keys(obj2);
|
||||
|
||||
if (obj1Keys.length !== obj2Keys.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let key of obj1Keys) {
|
||||
if (!obj2Keys.includes(key) || !deepObjectMatch(obj1[key], obj2[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (ask_variables_on_launch || launchConf.survey_enabled) {
|
||||
// we need to check to see if the extra vars are different from the defaults
|
||||
// but we'll need to do some normalization. Convert both to JSON objects
|
||||
// and then compare.
|
||||
|
||||
let jsonifiedExtraVars = {};
|
||||
let jsonifiedExtraData = {};
|
||||
|
||||
// extra_vars has to be a string
|
||||
if (typeof extra_vars === 'string') {
|
||||
if (
|
||||
extra_vars === '{}' ||
|
||||
extra_vars === 'null' ||
|
||||
extra_vars === '' ||
|
||||
extra_vars === '""'
|
||||
) {
|
||||
jsonifiedExtraVars = {};
|
||||
} else {
|
||||
try {
|
||||
// try to turn the string into json
|
||||
jsonifiedExtraVars = JSON.parse(extra_vars);
|
||||
} catch (jsonParseError) {
|
||||
try {
|
||||
// do safeLoad, which well error if not valid yaml
|
||||
jsonifiedExtraVars = jsyaml.safeLoad(extra_vars);
|
||||
} catch (yamlLoadError) {
|
||||
setContentError(yamlLoadError);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setContentError(
|
||||
Error(i18n._(t`Error parsing extra variables from the job template`))
|
||||
);
|
||||
}
|
||||
|
||||
// extra_data on a node can be either a string or an object...
|
||||
if (typeof nodeExtraData === 'string') {
|
||||
if (
|
||||
nodeExtraData === '{}' ||
|
||||
nodeExtraData === 'null' ||
|
||||
nodeExtraData === '' ||
|
||||
nodeExtraData === '""'
|
||||
) {
|
||||
jsonifiedExtraData = {};
|
||||
} else {
|
||||
try {
|
||||
// try to turn the string into json
|
||||
jsonifiedExtraData = JSON.parse(nodeExtraData);
|
||||
} catch (error) {
|
||||
try {
|
||||
// do safeLoad, which well error if not valid yaml
|
||||
jsonifiedExtraData = jsyaml.safeLoad(nodeExtraData);
|
||||
} catch (yamlLoadError) {
|
||||
setContentError(yamlLoadError);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof nodeExtraData === 'object') {
|
||||
jsonifiedExtraData = nodeExtraData;
|
||||
} else {
|
||||
setContentError(
|
||||
Error(i18n._(t`Error parsing extra variables from the node`))
|
||||
);
|
||||
}
|
||||
|
||||
if (!deepObjectMatch(jsonifiedExtraVars, jsonifiedExtraData)) {
|
||||
variablesOverridden = true;
|
||||
variablesToShow = jsyaml.safeDump(
|
||||
Object.assign(jsonifiedExtraVars, jsonifiedExtraData)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let credentialsToShow = summary_fields.credentials;
|
||||
|
||||
if (credentialOverridden) {
|
||||
credentialsToShow = [...nodeCredentials];
|
||||
|
||||
// adds vault_id to the credentials we get back from
|
||||
// fetching the JT
|
||||
launchConf.defaults.credentials.forEach(launchCred => {
|
||||
if (launchCred.vault_id) {
|
||||
summary_fields.credentials[
|
||||
summary_fields.credentials.findIndex(
|
||||
defaultCred => defaultCred.id === launchCred.id
|
||||
)
|
||||
].vault_id = launchCred.vault_id;
|
||||
}
|
||||
});
|
||||
|
||||
summary_fields.credentials.forEach(defaultCred => {
|
||||
if (
|
||||
!nodeCredentials.some(
|
||||
overrideCredential =>
|
||||
(defaultCred.kind === overrideCredential.kind &&
|
||||
(!defaultCred.vault_id && !overrideCredential.inputs.vault_id)) ||
|
||||
(defaultCred.vault_id &&
|
||||
overrideCredential.inputs.vault_id &&
|
||||
defaultCred.vault_id === overrideCredential.inputs.vault_id)
|
||||
)
|
||||
) {
|
||||
credentialsToShow.push(defaultCred);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let verbosityToShow = '';
|
||||
|
||||
optionsActions.GET.verbosity.choices.forEach(choice => {
|
||||
if (
|
||||
verbosityOverridden
|
||||
? choice[0] === nodeVerbosity
|
||||
: choice[0] === verbosity
|
||||
) {
|
||||
verbosityToShow = choice[1];
|
||||
}
|
||||
});
|
||||
|
||||
const jobTagsToShow = jobTagsOverridden ? nodeJobTags : job_tags;
|
||||
const skipTagsToShow = skipTagsOverridden ? nodeSkipTags : skip_tags;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Node Type`)} value={i18n._(t`Job Template`)} />
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
<Detail
|
||||
label={
|
||||
jobTypeOverridden ? (
|
||||
<Overridden>* {i18n._(t`Job Type`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Job Type`)
|
||||
)
|
||||
}
|
||||
value={jobTypeOverridden ? nodeJobType : job_type}
|
||||
/>
|
||||
<Detail
|
||||
label={
|
||||
inventoryOverridden ? (
|
||||
<Overridden>* {i18n._(t`Inventory`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Inventory`)
|
||||
)
|
||||
}
|
||||
value={
|
||||
inventoryOverridden
|
||||
? nodeSummaryFields.inventory.name
|
||||
: summary_fields.inventory.name
|
||||
}
|
||||
alwaysVisible={inventoryOverridden}
|
||||
/>
|
||||
{summary_fields.project && (
|
||||
<Detail
|
||||
label={i18n._(t`Project`)}
|
||||
value={summary_fields.project.name}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
label={
|
||||
scmBranchOverridden ? (
|
||||
<Overridden>* {i18n._(t`SCM Branch`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`SCM Branch`)
|
||||
)
|
||||
}
|
||||
value={scmBranchOverridden ? nodeScmBranch : scm_branch}
|
||||
alwaysVisible={scmBranchOverridden}
|
||||
/>
|
||||
<Detail label={i18n._(t`Playbook`)} value={playbook} />
|
||||
<Detail label={i18n._(t`Forks`)} value={forks || '0'} />
|
||||
<Detail
|
||||
label={
|
||||
limitOverridden ? (
|
||||
<Overridden>* {i18n._(t`Limit`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Limit`)
|
||||
)
|
||||
}
|
||||
value={limitOverridden ? nodeLimit : limit}
|
||||
alwaysVisible={limitOverridden}
|
||||
/>
|
||||
<Detail
|
||||
label={
|
||||
verbosityOverridden ? (
|
||||
<Overridden>* {i18n._(t`Verbosity`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Verbosity`)
|
||||
)
|
||||
}
|
||||
value={verbosityToShow}
|
||||
/>
|
||||
<Detail label={i18n._(t`Timeout`)} value={timeout || '0'} />
|
||||
<Detail
|
||||
label={
|
||||
diffModeOverridden ? (
|
||||
<Overridden>* {i18n._(t`Show Changes`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Show Changes`)
|
||||
)
|
||||
}
|
||||
value={
|
||||
(diffModeOverridden
|
||||
? nodeDiffMode
|
||||
: diff_mode)
|
||||
? i18n._(t`On`)
|
||||
: i18n._(t`Off`)
|
||||
}
|
||||
/>
|
||||
<Detail label={i18n._(t` Job Slicing`)} value={job_slice_count} />
|
||||
{host_config_key && (
|
||||
<>
|
||||
<Detail
|
||||
label={i18n._(t`Host Config Key`)}
|
||||
value={host_config_key}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Provisioning Callback URL`)}
|
||||
value={generateCallBackUrl}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Detail
|
||||
fullWidth
|
||||
label={
|
||||
credentialOverridden ? (
|
||||
<Overridden>* {i18n._(t`Credentials`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Credentials`)
|
||||
)
|
||||
}
|
||||
value={
|
||||
credentialsToShow.length > 0 && (
|
||||
<ChipGroup numChips={5}>
|
||||
{credentialsToShow.map(c => (
|
||||
<CredentialChip key={c.id} credential={c} isReadOnly />
|
||||
))}
|
||||
</ChipGroup>
|
||||
)
|
||||
}
|
||||
alwaysVisible={credentialOverridden}
|
||||
/>
|
||||
{summary_fields.labels && summary_fields.labels.results.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Labels`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{summary_fields.labels.results.map(l => (
|
||||
<Chip key={l.id} isReadOnly>
|
||||
{l.name}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{instanceGroups.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Instance Groups`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{instanceGroups.map(ig => (
|
||||
<Chip key={ig.id} isReadOnly>
|
||||
{ig.name}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
fullWidth
|
||||
label={
|
||||
jobTagsOverridden ? (
|
||||
<Overridden>* {i18n._(t`Job Tags`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Job Tags`)
|
||||
)
|
||||
}
|
||||
value={
|
||||
jobTagsOverridden.length > 0 && (
|
||||
<ChipGroup numChips={5}>
|
||||
{jobTagsToShow.split(',').map(jobTag => (
|
||||
<Chip key={jobTag} isReadOnly>
|
||||
{jobTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
)
|
||||
}
|
||||
alwaysVisible={jobTagsOverridden}
|
||||
/>
|
||||
<Detail
|
||||
fullWidth
|
||||
label={
|
||||
skipTagsOverridden ? (
|
||||
<Overridden>* {i18n._(t`Skip Tags`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Skip Tags`)
|
||||
)
|
||||
}
|
||||
value={
|
||||
skipTagsToShow.length > 0 && (
|
||||
<ChipGroup numChips={5}>
|
||||
{skipTagsToShow.split(',').map(skipTag => (
|
||||
<Chip key={skipTag} isReadOnly>
|
||||
{skipTag}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
)
|
||||
}
|
||||
alwaysVisible={skipTagsOverridden}
|
||||
/>
|
||||
<VariablesDetail
|
||||
label={
|
||||
variablesOverridden ? (
|
||||
<Overridden>* {i18n._(t`Variables`)}</Overridden>
|
||||
) : (
|
||||
i18n._(t`Variables`)
|
||||
)
|
||||
}
|
||||
value={variablesToShow}
|
||||
rows={4}
|
||||
/>
|
||||
</DetailList>
|
||||
{(jobTypeOverridden ||
|
||||
limitOverridden ||
|
||||
scmBranchOverridden ||
|
||||
inventoryOverridden ||
|
||||
verbosityOverridden ||
|
||||
jobTagsOverridden ||
|
||||
skipTagsOverridden ||
|
||||
diffModeOverridden ||
|
||||
credentialOverridden ||
|
||||
variablesOverridden) && (
|
||||
<>
|
||||
<br />
|
||||
<Overridden>
|
||||
<b>
|
||||
<Trans>
|
||||
* Values for these fields differ from the job template's default
|
||||
</Trans>
|
||||
</b>
|
||||
</Overridden>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(JobTemplateDetails);
|
||||
@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Modal } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import ApprovalDetails from './ApprovalDetails';
|
||||
import InventorySourceSyncDetails from './InventorySourceSyncDetails';
|
||||
import JobTemplateDetails from './JobTemplateDetails';
|
||||
import ProjectSyncDetails from './ProjectSyncDetails';
|
||||
import WorkflowJobTemplateDetails from './WorkflowJobTemplateDetails';
|
||||
|
||||
function NodeViewModal({ i18n, onClose, node }) {
|
||||
return (
|
||||
<Modal
|
||||
isLarge
|
||||
isOpen={true}
|
||||
title={i18n._(t`Node Details | ${node.unifiedJobTemplate.name}`)}
|
||||
onClose={onClose}
|
||||
>
|
||||
{(node.unifiedJobTemplate.type === 'job_template' || node.unifiedJobTemplate.unified_job_type === 'job') && (
|
||||
<JobTemplateDetails node={node} />
|
||||
)}
|
||||
{(node.unifiedJobTemplate.type === 'workflow_approval_template' || node.unifiedJobTemplate.unified_job_type) === 'workflow_approval' && (
|
||||
<ApprovalDetails node={node} />
|
||||
)}
|
||||
{(node.unifiedJobTemplate.type === 'project' || node.unifiedJobTemplate.unified_job_type === 'project_update') && (
|
||||
<ProjectSyncDetails node={node} />
|
||||
)}
|
||||
{(node.unifiedJobTemplate.type === 'inventory_source' || node.unifiedJobTemplate.unified_job_type === 'inventory_update') && (
|
||||
<InventorySourceSyncDetails node={node} />
|
||||
)}
|
||||
{(node.unifiedJobTemplate.type === 'workflow_job_template' || node.unifiedJobTemplate.unified_job_type === 'workflow_job') && (
|
||||
<WorkflowJobTemplateDetails node={node} />
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(NodeViewModal);
|
||||
@ -1,141 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { ProjectsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { CredentialChip } from '@components/Chip';
|
||||
import { toTitleCase } from '@util/strings';
|
||||
|
||||
function ProjectSyncDetails({ i18n, node }) {
|
||||
const [project, setProject] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [noReadAccess, setNoReadAccess] = useState(false);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchProject() {
|
||||
try {
|
||||
const { data } = await ProjectsAPI.readDetail(
|
||||
node.unifiedJobTemplate.id
|
||||
);
|
||||
setProject(data);
|
||||
} catch (err) {
|
||||
if (err.response.status === 403) {
|
||||
setNoReadAccess(true);
|
||||
} else {
|
||||
setContentError(err);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchProject();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
|
||||
if (noReadAccess) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<Trans>
|
||||
Your account does not have read access to this project so the
|
||||
displayed details will be limited.
|
||||
</Trans>
|
||||
</p>
|
||||
<br />
|
||||
<DetailList gutter="sm">
|
||||
<Detail
|
||||
label={i18n._(t`Node Type`)}
|
||||
value={i18n._(t`Project Sync`)}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Name`)}
|
||||
value={node.unifiedJobTemplate.name}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Description`)}
|
||||
value={node.unifiedJobTemplate.description}
|
||||
/>
|
||||
</DetailList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
custom_virtualenv,
|
||||
description,
|
||||
local_path,
|
||||
name,
|
||||
scm_branch,
|
||||
scm_refspec,
|
||||
scm_type,
|
||||
scm_update_cache_timeout,
|
||||
scm_url,
|
||||
summary_fields,
|
||||
} = project;
|
||||
|
||||
return (
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Node Type`)} value={i18n._(t`Project Sync`)} />
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
{summary_fields.organization && (
|
||||
<Detail
|
||||
label={i18n._(t`Organization`)}
|
||||
value={summary_fields.organization.name}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
label={i18n._(t`SCM Type`)}
|
||||
value={
|
||||
scm_type === '' ? i18n._(t`Manual`) : toTitleCase(project.scm_type)
|
||||
}
|
||||
/>
|
||||
<Detail label={i18n._(t`SCM URL`)} value={scm_url} />
|
||||
<Detail label={i18n._(t`SCM Branch`)} value={scm_branch} />
|
||||
<Detail label={i18n._(t`SCM Refspec`)} value={scm_refspec} />
|
||||
{summary_fields.credential && (
|
||||
<Detail
|
||||
label={i18n._(t`SCM Credential`)}
|
||||
value={
|
||||
<CredentialChip
|
||||
key={summary_fields.credential.id}
|
||||
credential={summary_fields.credential}
|
||||
isReadOnly
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
label={i18n._(t`Cache Timeout`)}
|
||||
value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Ansible Environment`)}
|
||||
value={custom_virtualenv}
|
||||
/>
|
||||
<Config>
|
||||
{({ project_base_dir }) => (
|
||||
<Detail
|
||||
label={i18n._(t`Project Base Path`)}
|
||||
value={project_base_dir}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
<Detail label={i18n._(t`Playbook Directory`)} value={local_path} />
|
||||
</DetailList>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(ProjectSyncDetails);
|
||||
@ -1,129 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { WorkflowJobTemplatesAPI } from '@api';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { ChipGroup, Chip } from '@components/Chip';
|
||||
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||
|
||||
function WorkflowJobTemplateDetails({ i18n, node }) {
|
||||
const [workflowJobTemplate, setWorkflowJobTemplate] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [noReadAccess, setNoReadAccess] = useState(false);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchWorkflowJobTemplate() {
|
||||
try {
|
||||
const { data } = await WorkflowJobTemplatesAPI.readDetail(
|
||||
node.unifiedJobTemplate.id
|
||||
);
|
||||
setWorkflowJobTemplate(data);
|
||||
} catch (err) {
|
||||
if (err.response.status === 403) {
|
||||
setNoReadAccess(true);
|
||||
} else {
|
||||
setContentError(err);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchWorkflowJobTemplate();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
|
||||
if (noReadAccess) {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<Trans>
|
||||
Your account does not have read access to this workflow job template
|
||||
so the displayed details will be limited.
|
||||
</Trans>
|
||||
</p>
|
||||
<br />
|
||||
<DetailList gutter="sm">
|
||||
<Detail
|
||||
label={i18n._(t`Node Type`)}
|
||||
value={i18n._(t`Workflow Job Template`)}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Name`)}
|
||||
value={node.unifiedJobTemplate.name}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Description`)}
|
||||
value={node.unifiedJobTemplate.description}
|
||||
/>
|
||||
</DetailList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
description,
|
||||
extra_vars,
|
||||
limit,
|
||||
name,
|
||||
scm_branch,
|
||||
summary_fields,
|
||||
} = workflowJobTemplate;
|
||||
|
||||
return (
|
||||
<DetailList gutter="sm">
|
||||
<Detail
|
||||
label={i18n._(t`Node Type`)}
|
||||
value={i18n._(t`Workflow Job Template`)}
|
||||
/>
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
{summary_fields.organization && (
|
||||
<Detail
|
||||
label={i18n._(t`Organization`)}
|
||||
value={summary_fields.organization.name}
|
||||
/>
|
||||
)}
|
||||
{summary_fields.inventory && (
|
||||
<Detail
|
||||
label={i18n._(t`Inventory`)}
|
||||
value={summary_fields.inventory.name}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`Limit`)} value={limit} />
|
||||
<Detail label={i18n._(t`SCM Branch`)} value={scm_branch} />
|
||||
{summary_fields.labels && summary_fields.labels.results.length > 0 && (
|
||||
<Detail
|
||||
fullWidth
|
||||
label={i18n._(t`Labels`)}
|
||||
value={
|
||||
<ChipGroup numChips={5}>
|
||||
{summary_fields.labels.results.map(l => (
|
||||
<Chip key={l.id} isReadOnly>
|
||||
{l.name}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<VariablesDetail
|
||||
label={i18n._(t`Variables`)}
|
||||
value={extra_vars}
|
||||
rows={4}
|
||||
/>
|
||||
</DetailList>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(WorkflowJobTemplateDetails);
|
||||
@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Button, Modal } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { func } from 'prop-types';
|
||||
|
||||
function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) {
|
||||
return (
|
||||
<Modal
|
||||
width={600}
|
||||
isOpen={true}
|
||||
isOpen
|
||||
title={i18n._(t`Warning: Unsaved Changes`)}
|
||||
onClose={onCancel}
|
||||
actions={[
|
||||
@ -40,4 +40,10 @@ function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) {
|
||||
);
|
||||
}
|
||||
|
||||
UnsavedChangesModal.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
onExit: func.isRequired,
|
||||
onSaveAndExit: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(UnsavedChangesModal);
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export { default as DeleteAllNodesModal } from './DeleteAllNodesModal';
|
||||
export { default as LinkDeleteModal } from './LinkDeleteModal';
|
||||
export { default as LinkModal } from './LinkModal';
|
||||
export { default as NodeDeleteModal } from './NodeDeleteModal';
|
||||
export { default as NodeViewModal } from './NodeViewModal';
|
||||
export { default as UnsavedChangesModal } from './UnsavedChangesModal';
|
||||
@ -3,31 +3,34 @@ import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { shape } from 'prop-types';
|
||||
import { BaseSizes, Title, TitleLevel } from '@patternfly/react-core';
|
||||
import { layoutGraph } from '@util/workflow';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import DeleteAllNodesModal from './Modals/DeleteAllNodesModal';
|
||||
import LinkModal from './Modals/LinkModal';
|
||||
import LinkDeleteModal from './Modals/LinkDeleteModal';
|
||||
import NodeModal from './Modals/NodeModal/NodeModal';
|
||||
import NodeDeleteModal from './Modals/NodeDeleteModal';
|
||||
import {
|
||||
DeleteAllNodesModal,
|
||||
LinkModal,
|
||||
LinkDeleteModal,
|
||||
NodeDeleteModal,
|
||||
NodeViewModal,
|
||||
UnsavedChangesModal,
|
||||
} from './Modals';
|
||||
import { NodeModal } from './Modals/NodeModal';
|
||||
import VisualizerGraph from './VisualizerGraph';
|
||||
import VisualizerStartScreen from './VisualizerStartScreen';
|
||||
import VisualizerToolbar from './VisualizerToolbar';
|
||||
import UnsavedChangesModal from './Modals/UnsavedChangesModal';
|
||||
import NodeViewModal from './Modals/NodeViewModal/NodeViewModal';
|
||||
import {
|
||||
WorkflowApprovalTemplatesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
WorkflowJobTemplateNodesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '@api';
|
||||
|
||||
const CenteredContent = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
@ -42,46 +45,42 @@ const fetchWorkflowNodes = async (
|
||||
pageNo = 1,
|
||||
workflowNodes = []
|
||||
) => {
|
||||
try {
|
||||
const { data } = await WorkflowJobTemplatesAPI.readNodes(templateId, {
|
||||
page_size: 200,
|
||||
page: pageNo,
|
||||
});
|
||||
if (data.next) {
|
||||
return await fetchWorkflowNodes(
|
||||
templateId,
|
||||
pageNo + 1,
|
||||
workflowNodes.concat(data.results)
|
||||
);
|
||||
}
|
||||
return workflowNodes.concat(data.results);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
const { data } = await WorkflowJobTemplatesAPI.readNodes(templateId, {
|
||||
page_size: 200,
|
||||
page: pageNo,
|
||||
});
|
||||
if (data.next) {
|
||||
return fetchWorkflowNodes(
|
||||
templateId,
|
||||
pageNo + 1,
|
||||
workflowNodes.concat(data.results)
|
||||
);
|
||||
}
|
||||
return workflowNodes.concat(data.results);
|
||||
};
|
||||
|
||||
function Visualizer({ history, template, i18n }) {
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [links, setLinks] = useState([]);
|
||||
const [nodes, setNodes] = useState([]);
|
||||
const [linkToDelete, setLinkToDelete] = useState(null);
|
||||
const [linkToEdit, setLinkToEdit] = useState(null);
|
||||
const [nodePositions, setNodePositions] = useState(null);
|
||||
const [nodeToDelete, setNodeToDelete] = useState(null);
|
||||
const [nodeToEdit, setNodeToEdit] = useState(null);
|
||||
const [nodeToView, setNodeToView] = useState(null);
|
||||
const [addingLink, setAddingLink] = useState(false);
|
||||
const [addLinkSourceNode, setAddLinkSourceNode] = useState(null);
|
||||
const [addLinkTargetNode, setAddLinkTargetNode] = useState(null);
|
||||
const [addNodeSource, setAddNodeSource] = useState(null);
|
||||
const [addNodeTarget, setAddNodeTarget] = useState(null);
|
||||
const [addingLink, setAddingLink] = useState(false);
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [linkToDelete, setLinkToDelete] = useState(null);
|
||||
const [linkToEdit, setLinkToEdit] = useState(null);
|
||||
const [links, setLinks] = useState([]);
|
||||
const [nextNodeId, setNextNodeId] = useState(0);
|
||||
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
||||
const [showUnsavedChangesModal, setShowUnsavedChangesModal] = useState(false);
|
||||
const [nodePositions, setNodePositions] = useState(null);
|
||||
const [nodeToDelete, setNodeToDelete] = useState(null);
|
||||
const [nodeToEdit, setNodeToEdit] = useState(null);
|
||||
const [nodeToView, setNodeToView] = useState(null);
|
||||
const [nodes, setNodes] = useState([]);
|
||||
const [showDeleteAllNodesModal, setShowDeleteAllNodesModal] = useState(false);
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [showTools, setShowTools] = useState(false);
|
||||
const [showUnsavedChangesModal, setShowUnsavedChangesModal] = useState(false);
|
||||
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
||||
|
||||
const startAddNode = (sourceNodeId, targetNodeId = null) => {
|
||||
setAddNodeSource(sourceNodeId);
|
||||
@ -100,13 +99,13 @@ function Visualizer({ history, template, i18n }) {
|
||||
// Ensures that root nodes appear to always run
|
||||
// after "START"
|
||||
if (addNodeSource === 1) {
|
||||
newNode.edgeType = 'always';
|
||||
newNode.linkType = 'always';
|
||||
}
|
||||
|
||||
newLinks.push({
|
||||
source: { id: addNodeSource },
|
||||
target: { id: nextNodeId },
|
||||
edgeType: newNode.edgeType,
|
||||
linkType: newNode.linkType,
|
||||
type: 'link',
|
||||
});
|
||||
if (addNodeTarget) {
|
||||
@ -129,8 +128,8 @@ function Visualizer({ history, template, i18n }) {
|
||||
setLinks(newLinks);
|
||||
};
|
||||
|
||||
const startEditNode = nodeToEdit => {
|
||||
setNodeToEdit(nodeToEdit);
|
||||
const startEditNode = node => {
|
||||
setNodeToEdit(node);
|
||||
};
|
||||
|
||||
const finishEditingNode = editedNode => {
|
||||
@ -175,7 +174,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
|
||||
if (link.source.id === nodeId || link.target.id === nodeId) {
|
||||
if (link.source.id === nodeId) {
|
||||
children.push({ id: link.target.id, edgeType: link.edgeType });
|
||||
children.push({ id: link.target.id, linkType: link.linkType });
|
||||
} else if (link.target.id === nodeId) {
|
||||
parents.push(link.source.id);
|
||||
}
|
||||
@ -193,7 +192,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
newLinks.push({
|
||||
source: { id: parentId },
|
||||
target: { id: child.id },
|
||||
edgeType: 'always',
|
||||
linkType: 'always',
|
||||
type: 'link',
|
||||
});
|
||||
}
|
||||
@ -201,7 +200,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
newLinks.push({
|
||||
source: { id: parentId },
|
||||
target: { id: child.id },
|
||||
edgeType: child.edgeType,
|
||||
linkType: child.linkType,
|
||||
type: 'link',
|
||||
});
|
||||
}
|
||||
@ -217,14 +216,14 @@ function Visualizer({ history, template, i18n }) {
|
||||
setLinks(newLinks);
|
||||
};
|
||||
|
||||
const updateLink = edgeType => {
|
||||
const updateLink = linkType => {
|
||||
const newLinks = [...links];
|
||||
newLinks.forEach(link => {
|
||||
if (
|
||||
link.source.id === linkToEdit.source.id &&
|
||||
link.target.id === linkToEdit.target.id
|
||||
) {
|
||||
link.edgeType = edgeType;
|
||||
link.linkType = linkType;
|
||||
}
|
||||
});
|
||||
|
||||
@ -236,12 +235,12 @@ function Visualizer({ history, template, i18n }) {
|
||||
};
|
||||
|
||||
const startDeleteLink = link => {
|
||||
let parentMap = {};
|
||||
links.forEach(link => {
|
||||
if (!parentMap[link.target.id]) {
|
||||
parentMap[link.target.id] = [];
|
||||
const parentMap = {};
|
||||
links.forEach(existingLink => {
|
||||
if (!parentMap[existingLink.target.id]) {
|
||||
parentMap[existingLink.target.id] = [];
|
||||
}
|
||||
parentMap[link.target.id].push(link.source.id);
|
||||
parentMap[existingLink.target.id].push(existingLink.source.id);
|
||||
});
|
||||
|
||||
link.isConvergenceLink = parentMap[link.target.id].length > 1;
|
||||
@ -272,7 +271,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
target: {
|
||||
id: linkToDelete.target.id,
|
||||
},
|
||||
edgeType: 'always',
|
||||
linkType: 'always',
|
||||
type: 'link',
|
||||
});
|
||||
}
|
||||
@ -286,8 +285,8 @@ function Visualizer({ history, template, i18n }) {
|
||||
|
||||
const selectSourceNodeForLinking = sourceNode => {
|
||||
const newNodes = [...nodes];
|
||||
let parentMap = {};
|
||||
let invalidLinkTargetIds = [];
|
||||
const parentMap = {};
|
||||
const invalidLinkTargetIds = [];
|
||||
// Find and mark any ancestors as disabled to prevent cycles
|
||||
links.forEach(link => {
|
||||
// id=1 is our artificial root node so we don't care about that
|
||||
@ -303,7 +302,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
}
|
||||
});
|
||||
|
||||
let getAncestors = id => {
|
||||
const getAncestors = id => {
|
||||
if (parentMap[id]) {
|
||||
parentMap[id].forEach(parentId => {
|
||||
invalidLinkTargetIds.push(parentId);
|
||||
@ -334,7 +333,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
setAddLinkTargetNode(targetNode);
|
||||
};
|
||||
|
||||
const addLink = edgeType => {
|
||||
const addLink = linkType => {
|
||||
const newLinks = [...links];
|
||||
const newNodes = [...nodes];
|
||||
|
||||
@ -345,7 +344,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
newLinks.push({
|
||||
source: { id: addLinkSourceNode.id },
|
||||
target: { id: addLinkTargetNode.id },
|
||||
edgeType,
|
||||
linkType,
|
||||
type: 'link',
|
||||
});
|
||||
|
||||
@ -513,7 +512,6 @@ function Visualizer({ history, template, i18n }) {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: error handling?
|
||||
await Promise.all(nodeRequests);
|
||||
await Promise.all(approvalTemplateRequests);
|
||||
|
||||
@ -529,8 +527,8 @@ function Visualizer({ history, template, i18n }) {
|
||||
if (!linkMap[realLinkSourceId]) {
|
||||
linkMap[realLinkSourceId] = {};
|
||||
}
|
||||
linkMap[realLinkSourceId][realLinkTargetId] = link.edgeType;
|
||||
switch (link.edgeType) {
|
||||
linkMap[realLinkSourceId][realLinkTargetId] = link.linkType;
|
||||
switch (link.linkType) {
|
||||
case 'success':
|
||||
if (
|
||||
!originalLinkMap[link.source.id].success_nodes.includes(
|
||||
@ -563,7 +561,8 @@ function Visualizer({ history, template, i18n }) {
|
||||
}
|
||||
});
|
||||
|
||||
for (const [nodeId, node] of Object.entries(originalLinkMap)) {
|
||||
Object.keys(originalLinkMap).forEach(key => {
|
||||
const node = originalLinkMap[key];
|
||||
node.success_nodes.forEach(successNodeId => {
|
||||
if (
|
||||
!deletedNodeIds.includes(successNodeId) &&
|
||||
@ -609,13 +608,12 @@ function Visualizer({ history, template, i18n }) {
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: error handling?
|
||||
await Promise.all(disassociateRequests);
|
||||
|
||||
newLinks.forEach(link => {
|
||||
switch (link.edgeType) {
|
||||
switch (link.linkType) {
|
||||
case 'success':
|
||||
associateRequests.push(
|
||||
WorkflowJobTemplateNodesAPI.associateSuccessNode(
|
||||
@ -644,7 +642,6 @@ function Visualizer({ history, template, i18n }) {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: error handling?
|
||||
await Promise.all(associateRequests);
|
||||
|
||||
// Some nodes (both new and edited) are going to need a followup request to
|
||||
@ -655,11 +652,8 @@ function Visualizer({ history, template, i18n }) {
|
||||
|
||||
useEffect(() => {
|
||||
const buildGraphArrays = workflowNodes => {
|
||||
const nonRootNodeIds = [];
|
||||
const allNodeIds = [];
|
||||
const arrayOfLinksForChart = [];
|
||||
const nodeIdToChartNodeIdMapping = {};
|
||||
const chartNodeIdToIndexMapping = {};
|
||||
const arrayOfNodesForChart = [
|
||||
{
|
||||
id: 1,
|
||||
@ -669,6 +663,9 @@ function Visualizer({ history, template, i18n }) {
|
||||
type: 'node',
|
||||
},
|
||||
];
|
||||
const chartNodeIdToIndexMapping = {};
|
||||
const nodeIdToChartNodeIdMapping = {};
|
||||
const nonRootNodeIds = [];
|
||||
let nodeIdCounter = 2;
|
||||
// Assign each node an ID - 1 is reserved for the start node. We need to
|
||||
// make sure that we have an ID on every node including new nodes so the
|
||||
@ -704,7 +701,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
edgeType: 'success',
|
||||
linkType: 'success',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
@ -715,7 +712,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
edgeType: 'failure',
|
||||
linkType: 'failure',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
@ -726,7 +723,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[sourceIndex],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
edgeType: 'always',
|
||||
linkType: 'always',
|
||||
type: 'link',
|
||||
});
|
||||
nonRootNodeIds.push(nodeId);
|
||||
@ -745,7 +742,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
arrayOfLinksForChart.push({
|
||||
source: arrayOfNodesForChart[0],
|
||||
target: arrayOfNodesForChart[targetIndex],
|
||||
edgeType: 'always',
|
||||
linkType: 'always',
|
||||
type: 'link',
|
||||
});
|
||||
});
|
||||
@ -803,33 +800,33 @@ function Visualizer({ history, template, i18n }) {
|
||||
<Fragment>
|
||||
<Wrapper>
|
||||
<VisualizerToolbar
|
||||
keyShown={showKey}
|
||||
nodes={nodes}
|
||||
template={template}
|
||||
onClose={handleVisualizerClose}
|
||||
onSave={handleVisualizerSave}
|
||||
onDeleteAllClick={() => setShowDeleteAllNodesModal(true)}
|
||||
onKeyToggle={() => setShowKey(!showKey)}
|
||||
keyShown={showKey}
|
||||
onSave={handleVisualizerSave}
|
||||
onToolsToggle={() => setShowTools(!showTools)}
|
||||
template={template}
|
||||
toolsShown={showTools}
|
||||
/>
|
||||
{links.length > 0 ? (
|
||||
<VisualizerGraph
|
||||
links={links}
|
||||
nodes={nodes}
|
||||
nodePositions={nodePositions}
|
||||
readOnly={!template.summary_fields.user_capabilities.edit}
|
||||
onAddNodeClick={startAddNode}
|
||||
onEditNodeClick={startEditNode}
|
||||
onDeleteNodeClick={setNodeToDelete}
|
||||
onLinkEditClick={setLinkToEdit}
|
||||
onDeleteLinkClick={startDeleteLink}
|
||||
onStartAddLinkClick={selectSourceNodeForLinking}
|
||||
onConfirmAddLinkClick={selectTargetNodeForLinking}
|
||||
onCancelAddLinkClick={cancelNodeLink}
|
||||
onViewNodeClick={setNodeToView}
|
||||
addingLink={addingLink}
|
||||
addLinkSourceNode={addLinkSourceNode}
|
||||
addingLink={addingLink}
|
||||
links={links}
|
||||
nodePositions={nodePositions}
|
||||
nodes={nodes}
|
||||
onAddNodeClick={startAddNode}
|
||||
onCancelAddLinkClick={cancelNodeLink}
|
||||
onConfirmAddLinkClick={selectTargetNodeForLinking}
|
||||
onDeleteLinkClick={startDeleteLink}
|
||||
onDeleteNodeClick={setNodeToDelete}
|
||||
onEditNodeClick={startEditNode}
|
||||
onLinkEditClick={setLinkToEdit}
|
||||
onStartAddLinkClick={selectSourceNodeForLinking}
|
||||
onViewNodeClick={setNodeToView}
|
||||
readOnly={!template.summary_fields.user_capabilities.edit}
|
||||
showKey={showKey}
|
||||
showTools={showTools}
|
||||
/>
|
||||
@ -837,58 +834,58 @@ function Visualizer({ history, template, i18n }) {
|
||||
<VisualizerStartScreen onStartClick={startAddNode} />
|
||||
)}
|
||||
</Wrapper>
|
||||
<NodeDeleteModal
|
||||
nodeToDelete={nodeToDelete}
|
||||
onConfirm={deleteNode}
|
||||
onCancel={() => setNodeToDelete(null)}
|
||||
/>
|
||||
{nodeToDelete && (
|
||||
<NodeDeleteModal
|
||||
nodeToDelete={nodeToDelete}
|
||||
onCancel={() => setNodeToDelete(null)}
|
||||
onConfirm={deleteNode}
|
||||
/>
|
||||
)}
|
||||
{linkToDelete && (
|
||||
<LinkDeleteModal
|
||||
linkToDelete={linkToDelete}
|
||||
onConfirm={deleteLink}
|
||||
onCancel={() => setLinkToDelete(null)}
|
||||
onConfirm={deleteLink}
|
||||
/>
|
||||
)}
|
||||
{linkToEdit && (
|
||||
<LinkModal
|
||||
linkType={linkToEdit.linkType}
|
||||
header={
|
||||
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
|
||||
{/* todo: make title match mockups (display: flex) */}
|
||||
{i18n._(t`Edit Link`)}
|
||||
</Title>
|
||||
}
|
||||
onConfirm={updateLink}
|
||||
onCancel={() => setLinkToEdit(null)}
|
||||
edgeType={linkToEdit.edgeType}
|
||||
onConfirm={updateLink}
|
||||
/>
|
||||
)}
|
||||
{addLinkSourceNode && addLinkTargetNode && (
|
||||
<LinkModal
|
||||
header={
|
||||
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
|
||||
{/* todo: make title match mockups (display: flex) */}
|
||||
{i18n._(t`Add Link`)}
|
||||
</Title>
|
||||
}
|
||||
onConfirm={addLink}
|
||||
onCancel={cancelNodeLink}
|
||||
onConfirm={addLink}
|
||||
/>
|
||||
)}
|
||||
{addNodeSource && (
|
||||
<NodeModal
|
||||
askLinkType={addNodeSource !== 1}
|
||||
title={i18n._(t`Add Node`)}
|
||||
onClose={() => cancelNodeForm()}
|
||||
onSave={finishAddingNode}
|
||||
title={i18n._(t`Add Node`)}
|
||||
/>
|
||||
)}
|
||||
{nodeToEdit && (
|
||||
<NodeModal
|
||||
askLinkType={false}
|
||||
node={nodeToEdit}
|
||||
title={i18n._(t`Edit Node`)}
|
||||
nodeToEdit={nodeToEdit}
|
||||
onClose={() => cancelNodeForm()}
|
||||
onSave={finishEditingNode}
|
||||
title={i18n._(t`Edit Node`)}
|
||||
/>
|
||||
)}
|
||||
{showUnsavedChangesModal && (
|
||||
@ -915,4 +912,8 @@ function Visualizer({ history, template, i18n }) {
|
||||
);
|
||||
}
|
||||
|
||||
Visualizer.propTypes = {
|
||||
template: shape().isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(withRouter(Visualizer));
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { arrayOf, bool, func, shape } from 'prop-types';
|
||||
import * as d3 from 'd3';
|
||||
import {
|
||||
calcZoomAndFit,
|
||||
@ -10,15 +11,15 @@ import {
|
||||
} from '@util/workflow';
|
||||
import {
|
||||
WorkflowHelp,
|
||||
WorkflowKey,
|
||||
WorkflowLinkHelp,
|
||||
WorkflowNodeHelp,
|
||||
WorkflowTools,
|
||||
} from '@components/Workflow';
|
||||
import {
|
||||
VisualizerLink,
|
||||
VisualizerNode,
|
||||
VisualizerStartNode,
|
||||
VisualizerKey,
|
||||
VisualizerTools,
|
||||
} from '@screens/Template/WorkflowJobTemplateVisualizer';
|
||||
|
||||
const PotentialLink = styled.polyline`
|
||||
@ -26,34 +27,34 @@ const PotentialLink = styled.polyline`
|
||||
`;
|
||||
|
||||
const WorkflowSVG = styled.svg`
|
||||
background-color: #f6f6f6;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background-color: #f6f6f6;
|
||||
`;
|
||||
|
||||
function VisualizerGraph({
|
||||
addLinkSourceNode,
|
||||
addingLink,
|
||||
i18n,
|
||||
links,
|
||||
nodes,
|
||||
readOnly,
|
||||
nodePositions,
|
||||
onDeleteNodeClick,
|
||||
nodes,
|
||||
onAddNodeClick,
|
||||
onCancelAddLinkClick,
|
||||
onConfirmAddLinkClick,
|
||||
onDeleteLinkClick,
|
||||
onDeleteNodeClick,
|
||||
onEditNodeClick,
|
||||
onLinkEditClick,
|
||||
onDeleteLinkClick,
|
||||
onStartAddLinkClick,
|
||||
onConfirmAddLinkClick,
|
||||
onCancelAddLinkClick,
|
||||
onViewNodeClick,
|
||||
addingLink,
|
||||
addLinkSourceNode,
|
||||
readOnly,
|
||||
showKey,
|
||||
showTools,
|
||||
i18n,
|
||||
}) {
|
||||
const [helpText, setHelpText] = useState(null);
|
||||
const [nodeHelp, setNodeHelp] = useState();
|
||||
const [linkHelp, setLinkHelp] = useState();
|
||||
const [nodeHelp, setNodeHelp] = useState();
|
||||
const [zoomPercentage, setZoomPercentage] = useState(100);
|
||||
const svgRef = useRef(null);
|
||||
const gRef = useRef(null);
|
||||
@ -115,9 +116,10 @@ function VisualizerGraph({
|
||||
};
|
||||
|
||||
const handlePan = direction => {
|
||||
let { x: xPos, y: yPos, k: currentScale } = d3.zoomTransform(
|
||||
d3.select(svgRef.current).node()
|
||||
);
|
||||
const transform = d3.zoomTransform(d3.select(svgRef.current).node());
|
||||
|
||||
let { x: xPos, y: yPos } = transform;
|
||||
const { k: currentScale } = transform;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
@ -223,23 +225,23 @@ function VisualizerGraph({
|
||||
<WorkflowSVG id="workflow-svg" ref={svgRef} css="">
|
||||
<defs>
|
||||
<marker
|
||||
id="workflow-triangle"
|
||||
className="WorkflowChart-noPointerEvents"
|
||||
viewBox="0 -5 10 10"
|
||||
refX="10"
|
||||
id="workflow-triangle"
|
||||
markerHeight="6"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="6"
|
||||
markerHeight="6"
|
||||
orient="auto"
|
||||
refX="10"
|
||||
viewBox="0 -5 10 10"
|
||||
>
|
||||
<path d="M0,-5L10,0L0,5" fill="#93969A" />
|
||||
</marker>
|
||||
</defs>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
opacity="0"
|
||||
id="workflow-backround"
|
||||
opacity="0"
|
||||
width="100%"
|
||||
{...(addingLink && {
|
||||
onMouseMove: e => drawPotentialLinkToCursor(e),
|
||||
onMouseOver: () =>
|
||||
@ -255,12 +257,12 @@ function VisualizerGraph({
|
||||
<g id="workflow-g" ref={gRef}>
|
||||
{nodePositions && [
|
||||
<VisualizerStartNode
|
||||
addingLink={addingLink}
|
||||
key="start"
|
||||
nodePositions={nodePositions}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
readOnly={readOnly}
|
||||
updateHelpText={setHelpText}
|
||||
addingLink={addingLink}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
/>,
|
||||
links.map(link => {
|
||||
if (
|
||||
@ -269,16 +271,16 @@ function VisualizerGraph({
|
||||
) {
|
||||
return (
|
||||
<VisualizerLink
|
||||
addingLink={addingLink}
|
||||
key={`link-${link.source.id}-${link.target.id}`}
|
||||
link={link}
|
||||
nodePositions={nodePositions}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
onDeleteLinkClick={onDeleteLinkClick}
|
||||
onLinkEditClick={onLinkEditClick}
|
||||
readOnly={readOnly}
|
||||
updateHelpText={setHelpText}
|
||||
updateLinkHelp={setLinkHelp}
|
||||
readOnly={readOnly}
|
||||
onLinkEditClick={onLinkEditClick}
|
||||
onDeleteLinkClick={onDeleteLinkClick}
|
||||
addingLink={addingLink}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -288,22 +290,22 @@ function VisualizerGraph({
|
||||
if (node.id > 1 && nodePositions[node.id] && !node.isDeleted) {
|
||||
return (
|
||||
<VisualizerNode
|
||||
key={`node-${node.id}`}
|
||||
node={node}
|
||||
nodePositions={nodePositions}
|
||||
updateHelpText={setHelpText}
|
||||
updateNodeHelp={setNodeHelp}
|
||||
readOnly={readOnly}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
onEditNodeClick={onEditNodeClick}
|
||||
onDeleteNodeClick={onDeleteNodeClick}
|
||||
onStartAddLinkClick={onStartAddLinkClick}
|
||||
onConfirmAddLinkClick={onConfirmAddLinkClick}
|
||||
onViewNodeClick={onViewNodeClick}
|
||||
addingLink={addingLink}
|
||||
isAddLinkSourceNode={
|
||||
addLinkSourceNode && addLinkSourceNode.id === node.id
|
||||
}
|
||||
key={`node-${node.id}`}
|
||||
node={node}
|
||||
nodePositions={nodePositions}
|
||||
onAddNodeClick={onAddNodeClick}
|
||||
onConfirmAddLinkClick={onConfirmAddLinkClick}
|
||||
onDeleteNodeClick={onDeleteNodeClick}
|
||||
onEditNodeClick={onEditNodeClick}
|
||||
onStartAddLinkClick={onStartAddLinkClick}
|
||||
onViewNodeClick={onViewNodeClick}
|
||||
readOnly={readOnly}
|
||||
updateHelpText={setHelpText}
|
||||
updateNodeHelp={setNodeHelp}
|
||||
{...(addingLink && {
|
||||
onMouseOver: () => drawPotentialLinkToNode(node),
|
||||
})}
|
||||
@ -316,28 +318,52 @@ function VisualizerGraph({
|
||||
{addingLink && (
|
||||
<PotentialLink
|
||||
id="workflow-potentialLink"
|
||||
markerEnd="url(#workflow-triangle)"
|
||||
stroke="#93969A"
|
||||
strokeDasharray="5,5"
|
||||
strokeWidth="2"
|
||||
stroke="#93969A"
|
||||
markerEnd="url(#workflow-triangle)"
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
</WorkflowSVG>
|
||||
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
||||
{showTools && (
|
||||
<VisualizerTools
|
||||
zoomPercentage={zoomPercentage}
|
||||
onZoomChange={handleZoomChange}
|
||||
<WorkflowTools
|
||||
onFitGraph={handleFitGraph}
|
||||
onPan={handlePan}
|
||||
onPanToMiddle={handlePanToMiddle}
|
||||
onZoomChange={handleZoomChange}
|
||||
zoomPercentage={zoomPercentage}
|
||||
/>
|
||||
)}
|
||||
{showKey && <VisualizerKey />}
|
||||
{showKey && <WorkflowKey />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
VisualizerGraph.propTypes = {
|
||||
addLinkSourceNode: shape(),
|
||||
addingLink: bool.isRequired,
|
||||
links: arrayOf(shape()).isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
nodes: arrayOf(shape()).isRequired,
|
||||
onAddNodeClick: func.isRequired,
|
||||
onCancelAddLinkClick: func.isRequired,
|
||||
onConfirmAddLinkClick: func.isRequired,
|
||||
onDeleteLinkClick: func.isRequired,
|
||||
onDeleteNodeClick: func.isRequired,
|
||||
onEditNodeClick: func.isRequired,
|
||||
onLinkEditClick: func.isRequired,
|
||||
onStartAddLinkClick: func.isRequired,
|
||||
onViewNodeClick: func.isRequired,
|
||||
readOnly: bool.isRequired,
|
||||
showKey: bool.isRequired,
|
||||
showTools: bool.isRequired,
|
||||
};
|
||||
|
||||
VisualizerGraph.defaultProps = {
|
||||
addLinkSourceNode: {},
|
||||
};
|
||||
|
||||
export default withI18n()(VisualizerGraph);
|
||||
|
||||
@ -2,11 +2,12 @@ import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { bool, func, shape } from 'prop-types';
|
||||
import { PencilAltIcon, PlusIcon, TrashAltIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
generateLine,
|
||||
getLinkOverlayPoints,
|
||||
getLinePoints,
|
||||
getLinkOverlayPoints,
|
||||
} from '@util/workflow';
|
||||
import {
|
||||
WorkflowActionTooltip,
|
||||
@ -18,16 +19,16 @@ const LinkG = styled.g`
|
||||
`;
|
||||
|
||||
function VisualizerLink({
|
||||
addingLink,
|
||||
i18n,
|
||||
link,
|
||||
nodePositions,
|
||||
onAddNodeClick,
|
||||
onDeleteLinkClick,
|
||||
onLinkEditClick,
|
||||
readOnly,
|
||||
updateHelpText,
|
||||
updateLinkHelp,
|
||||
i18n,
|
||||
onLinkEditClick,
|
||||
onDeleteLinkClick,
|
||||
addingLink,
|
||||
onAddNodeClick,
|
||||
}) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const [pathD, setPathD] = useState();
|
||||
@ -61,18 +62,18 @@ function VisualizerLink({
|
||||
<WorkflowActionTooltipItem
|
||||
id="link-edit"
|
||||
key="edit"
|
||||
onClick={() => onLinkEditClick(link)}
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Edit this link`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
onClick={() => onLinkEditClick(link)}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</WorkflowActionTooltipItem>,
|
||||
<WorkflowActionTooltipItem
|
||||
id="link-delete"
|
||||
key="delete"
|
||||
onClick={() => onDeleteLinkClick(link)}
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Delete this link`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
onClick={() => onDeleteLinkClick(link)}
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</WorkflowActionTooltipItem>,
|
||||
@ -95,16 +96,16 @@ function VisualizerLink({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (link.edgeType === 'failure') {
|
||||
if (link.linkType === 'failure') {
|
||||
setPathStroke('#d9534f');
|
||||
}
|
||||
if (link.edgeType === 'success') {
|
||||
if (link.linkType === 'success') {
|
||||
setPathStroke('#5cb85c');
|
||||
}
|
||||
if (link.edgeType === 'always') {
|
||||
if (link.linkType === 'always') {
|
||||
setPathStroke('#337ab7');
|
||||
}
|
||||
}, [link.edgeType]);
|
||||
}, [link.linkType]);
|
||||
|
||||
useEffect(() => {
|
||||
const linePoints = getLinePoints(link, nodePositions);
|
||||
@ -117,13 +118,13 @@ function VisualizerLink({
|
||||
<LinkG
|
||||
className="WorkflowGraph-link"
|
||||
id={`link-${link.source.id}-${link.target.id}`}
|
||||
ignorePointerEvents={addingLink}
|
||||
onMouseEnter={handleLinkMouseEnter}
|
||||
onMouseLeave={handleLinkMouseLeave}
|
||||
ignorePointerEvents={addingLink}
|
||||
>
|
||||
<polygon
|
||||
id={`link-${link.source.id}-${link.target.id}-overlay`}
|
||||
fill="#E1E1E1"
|
||||
id={`link-${link.source.id}-${link.target.id}-overlay`}
|
||||
opacity={hovering ? '1' : '0'}
|
||||
points={getLinkOverlayPoints(link, nodePositions)}
|
||||
/>
|
||||
@ -134,20 +135,32 @@ function VisualizerLink({
|
||||
strokeWidth="2px"
|
||||
/>
|
||||
<polygon
|
||||
opacity="0"
|
||||
points={getLinkOverlayPoints(link, nodePositions)}
|
||||
onMouseEnter={() => updateLinkHelp(link)}
|
||||
onMouseLeave={() => updateLinkHelp(null)}
|
||||
opacity="0"
|
||||
points={getLinkOverlayPoints(link, nodePositions)}
|
||||
/>
|
||||
{!readOnly && hovering && (
|
||||
<WorkflowActionTooltip
|
||||
actions={tooltipActions}
|
||||
pointX={tooltipX}
|
||||
pointY={tooltipY}
|
||||
actions={tooltipActions}
|
||||
/>
|
||||
)}
|
||||
</LinkG>
|
||||
);
|
||||
}
|
||||
|
||||
VisualizerLink.propTypes = {
|
||||
addingLink: bool.isRequired,
|
||||
link: shape().isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
onAddNodeClick: func.isRequired,
|
||||
onDeleteLinkClick: func.isRequired,
|
||||
onLinkEditClick: func.isRequired,
|
||||
readOnly: bool.isRequired,
|
||||
updateHelpText: func.isRequired,
|
||||
updateLinkHelp: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(VisualizerLink);
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { bool, func, shape } from 'prop-types';
|
||||
import {
|
||||
InfoIcon,
|
||||
LinkIcon,
|
||||
@ -30,28 +31,28 @@ const NodeContents = styled.foreignObject`
|
||||
|
||||
const NodeDefaultLabel = styled.p`
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
function VisualizerNode({
|
||||
addingLink,
|
||||
i18n,
|
||||
isAddLinkSourceNode,
|
||||
node,
|
||||
nodePositions,
|
||||
onAddNodeClick,
|
||||
onConfirmAddLinkClick,
|
||||
onDeleteNodeClick,
|
||||
onEditNodeClick,
|
||||
onMouseOver,
|
||||
onStartAddLinkClick,
|
||||
onViewNodeClick,
|
||||
readOnly,
|
||||
updateHelpText,
|
||||
updateNodeHelp,
|
||||
readOnly,
|
||||
i18n,
|
||||
onDeleteNodeClick,
|
||||
onStartAddLinkClick,
|
||||
onConfirmAddLinkClick,
|
||||
addingLink,
|
||||
onMouseOver,
|
||||
isAddLinkSourceNode,
|
||||
onAddNodeClick,
|
||||
onEditNodeClick,
|
||||
onViewNodeClick,
|
||||
}) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
||||
@ -88,13 +89,13 @@ function VisualizerNode({
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-details"
|
||||
key="details"
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
onClick={() => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
onViewNodeClick(node);
|
||||
}}
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
>
|
||||
<InfoIcon />
|
||||
</WorkflowActionTooltipItem>
|
||||
@ -106,13 +107,13 @@ function VisualizerNode({
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-add"
|
||||
key="add"
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Add a new node`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
onClick={() => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
onAddNodeClick(node.id);
|
||||
}}
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Add a new node`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
>
|
||||
<PlusIcon />
|
||||
</WorkflowActionTooltipItem>,
|
||||
@ -120,41 +121,41 @@ function VisualizerNode({
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-edit"
|
||||
key="edit"
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Edit this node`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
onClick={() => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
onEditNodeClick(node);
|
||||
}}
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Edit this node`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
>
|
||||
<PencilAltIcon />
|
||||
</WorkflowActionTooltipItem>,
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-link"
|
||||
key="link"
|
||||
onMouseEnter={() =>
|
||||
updateHelpText(i18n._(t`Link to an available node`))
|
||||
}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
onClick={() => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
onStartAddLinkClick(node);
|
||||
}}
|
||||
onMouseEnter={() =>
|
||||
updateHelpText(i18n._(t`Link to an available node`))
|
||||
}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
>
|
||||
<LinkIcon />
|
||||
</WorkflowActionTooltipItem>,
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-delete"
|
||||
key="delete"
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Delete this node`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
onClick={() => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
onDeleteNodeClick(node);
|
||||
}}
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`Delete this node`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</WorkflowActionTooltipItem>,
|
||||
@ -163,15 +164,15 @@ function VisualizerNode({
|
||||
return (
|
||||
<NodeG
|
||||
id={`node-${node.id}`}
|
||||
transform={`translate(${nodePositions[node.id].x},${nodePositions[node.id]
|
||||
.y - nodePositions[1].y})`}
|
||||
job={node.job}
|
||||
noPointerEvents={isAddLinkSourceNode}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={handleNodeMouseLeave}
|
||||
transform={`translate(${nodePositions[node.id].x},${nodePositions[node.id]
|
||||
.y - nodePositions[1].y})`}
|
||||
>
|
||||
<rect
|
||||
width={wfConstants.nodeW}
|
||||
fill="#FFFFFF"
|
||||
height={wfConstants.nodeH}
|
||||
rx="2"
|
||||
ry="2"
|
||||
@ -181,17 +182,17 @@ function VisualizerNode({
|
||||
: '#93969A'
|
||||
}
|
||||
strokeWidth="4px"
|
||||
fill="#FFFFFF"
|
||||
width={wfConstants.nodeW}
|
||||
/>
|
||||
<NodeContents
|
||||
height="60"
|
||||
width="180"
|
||||
isInvalidLinkTarget={node.isInvalidLinkTarget}
|
||||
{...(!addingLink && {
|
||||
onMouseEnter: () => updateNodeHelp(node),
|
||||
onMouseLeave: () => updateNodeHelp(null),
|
||||
})}
|
||||
onClick={() => handleNodeClick()}
|
||||
width="180"
|
||||
>
|
||||
<NodeDefaultLabel>
|
||||
{node.unifiedJobTemplate
|
||||
@ -211,4 +212,26 @@ function VisualizerNode({
|
||||
);
|
||||
}
|
||||
|
||||
VisualizerNode.propTypes = {
|
||||
addingLink: bool.isRequired,
|
||||
isAddLinkSourceNode: bool,
|
||||
node: shape().isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
onAddNodeClick: func.isRequired,
|
||||
onConfirmAddLinkClick: func.isRequired,
|
||||
onDeleteNodeClick: func.isRequired,
|
||||
onEditNodeClick: func.isRequired,
|
||||
onMouseOver: func,
|
||||
onStartAddLinkClick: func.isRequired,
|
||||
onViewNodeClick: func.isRequired,
|
||||
readOnly: bool.isRequired,
|
||||
updateHelpText: func.isRequired,
|
||||
updateNodeHelp: func.isRequired,
|
||||
};
|
||||
|
||||
VisualizerNode.defaultProps = {
|
||||
isAddLinkSourceNode: false,
|
||||
onMouseOver: () => {},
|
||||
};
|
||||
|
||||
export default withI18n()(VisualizerNode);
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { bool, func, shape } from 'prop-types';
|
||||
import { PlusIcon } from '@patternfly/react-icons';
|
||||
import { constants as wfConstants } from '@util/workflow';
|
||||
import {
|
||||
@ -14,12 +15,12 @@ const StartG = styled.g`
|
||||
`;
|
||||
|
||||
function VisualizerStartNode({
|
||||
updateHelpText,
|
||||
nodePositions,
|
||||
readOnly,
|
||||
i18n,
|
||||
addingLink,
|
||||
i18n,
|
||||
nodePositions,
|
||||
onAddNodeClick,
|
||||
readOnly,
|
||||
updateHelpText,
|
||||
}) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
||||
@ -32,18 +33,18 @@ function VisualizerStartNode({
|
||||
return (
|
||||
<StartG
|
||||
id="node-1"
|
||||
transform={`translate(${nodePositions[1].x},0)`}
|
||||
ignorePointerEvents={addingLink}
|
||||
onMouseEnter={handleNodeMouseEnter}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
ignorePointerEvents={addingLink}
|
||||
transform={`translate(${nodePositions[1].x},0)`}
|
||||
>
|
||||
<rect
|
||||
width={wfConstants.rootW}
|
||||
fill="#0279BC"
|
||||
height={wfConstants.rootH}
|
||||
y="10"
|
||||
rx="2"
|
||||
ry="2"
|
||||
fill="#0279BC"
|
||||
width={wfConstants.rootW}
|
||||
y="10"
|
||||
/>
|
||||
{/* TODO: We need to be able to handle translated text here */}
|
||||
<text x="13" y="30" dy=".35em" fill="white">
|
||||
@ -51,8 +52,6 @@ function VisualizerStartNode({
|
||||
</text>
|
||||
{!readOnly && hovering && (
|
||||
<WorkflowActionTooltip
|
||||
pointX={wfConstants.rootW}
|
||||
pointY={wfConstants.rootH / 2 + 10}
|
||||
actions={[
|
||||
<WorkflowActionTooltipItem
|
||||
id="node-add"
|
||||
@ -68,10 +67,20 @@ function VisualizerStartNode({
|
||||
<PlusIcon />
|
||||
</WorkflowActionTooltipItem>,
|
||||
]}
|
||||
pointX={wfConstants.rootW}
|
||||
pointY={wfConstants.rootH / 2 + 10}
|
||||
/>
|
||||
)}
|
||||
</StartG>
|
||||
);
|
||||
}
|
||||
|
||||
VisualizerStartNode.propTypes = {
|
||||
addingLink: bool.isRequired,
|
||||
nodePositions: shape().isRequired,
|
||||
onAddNodeClick: func.isRequired,
|
||||
readOnly: bool.isRequired,
|
||||
updateHelpText: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(VisualizerStartNode);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func } from 'prop-types';
|
||||
import { Button as PFButton } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -14,30 +15,30 @@ const Button = styled(PFButton)`
|
||||
`;
|
||||
|
||||
const StartPanel = styled.div`
|
||||
padding: 60px 80px;
|
||||
border: 1px solid #c7c7c7;
|
||||
background-color: white;
|
||||
border: 1px solid #c7c7c7;
|
||||
padding: 60px 80px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StartPanelWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background-color: #f6f6f6;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
function StartScreen({ i18n, onStartClick }) {
|
||||
function VisualizerStartScreen({ i18n, onStartClick }) {
|
||||
return (
|
||||
<div css="flex: 1">
|
||||
<StartPanelWrapper>
|
||||
<StartPanel>
|
||||
<p>{i18n._(t`Please click the Start button to begin.`)}</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label={i18n._(t`Start`)}
|
||||
onClick={() => onStartClick(1)}
|
||||
variant="primary"
|
||||
>
|
||||
{i18n._(t`Start`)}
|
||||
</Button>
|
||||
@ -47,4 +48,8 @@ function StartScreen({ i18n, onStartClick }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(StartScreen);
|
||||
VisualizerStartScreen.propTypes = {
|
||||
onStartClick: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(VisualizerStartScreen);
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { arrayOf, bool, func, shape } from 'prop-types';
|
||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||
import {
|
||||
BookIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
RocketIcon,
|
||||
TimesIcon,
|
||||
TrashAltIcon,
|
||||
@ -36,68 +36,50 @@ const ActionButton = styled(Button)`
|
||||
}
|
||||
`;
|
||||
|
||||
function Toolbar({
|
||||
function VisualizerToolbar({
|
||||
i18n,
|
||||
template,
|
||||
keyShown,
|
||||
nodes,
|
||||
onClose,
|
||||
onSave,
|
||||
nodes = [],
|
||||
onDeleteAllClick,
|
||||
onKeyToggle,
|
||||
keyShown,
|
||||
onSave,
|
||||
onToolsToggle,
|
||||
template,
|
||||
toolsShown,
|
||||
}) {
|
||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
borderBottom: '1px solid grey',
|
||||
height: '56px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0px 20px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div css="align-items: center; border-bottom: 1px solid grey; display: flex; height: 56px; padding: 0px 20px;">
|
||||
<div css="display: flex;">
|
||||
<b>{i18n._(t`Workflow Visualizer`)}</b>
|
||||
<VerticalSeparator />
|
||||
<b>{template.name}</b>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: '1',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div css="align-items: center; display: flex; flex: 1; justify-content: flex-end">
|
||||
<div>{i18n._(t`Total Nodes`)}</div>
|
||||
<Badge isRead>{totalNodes}</Badge>
|
||||
<VerticalSeparator />
|
||||
<Tooltip content={i18n._(t`Toggle Key`)} position="bottom">
|
||||
<ActionButton
|
||||
variant="plain"
|
||||
onClick={onKeyToggle}
|
||||
isActive={keyShown}
|
||||
onClick={onKeyToggle}
|
||||
variant="plain"
|
||||
>
|
||||
<CompassIcon />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
||||
<ActionButton
|
||||
variant="plain"
|
||||
onClick={onToolsToggle}
|
||||
isActive={toolsShown}
|
||||
onClick={onToolsToggle}
|
||||
variant="plain"
|
||||
>
|
||||
<WrenchIcon />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<ActionButton variant="plain" isDisabled>
|
||||
<DownloadIcon />
|
||||
</ActionButton>
|
||||
<ActionButton variant="plain" isDisabled>
|
||||
<BookIcon />
|
||||
</ActionButton>
|
||||
@ -106,10 +88,10 @@ function Toolbar({
|
||||
</ActionButton>
|
||||
<Tooltip content={i18n._(t`Delete All Nodes`)} position="bottom">
|
||||
<ActionButton
|
||||
variant="plain"
|
||||
isDisabled={totalNodes === 0}
|
||||
aria-label={i18n._(t`Delete all nodes`)}
|
||||
isDisabled={totalNodes === 0}
|
||||
onClick={onDeleteAllClick}
|
||||
variant="plain"
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</ActionButton>
|
||||
@ -120,9 +102,9 @@ function Toolbar({
|
||||
</Button>
|
||||
<VerticalSeparator />
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label={i18n._(t`Close`)}
|
||||
onClick={onClose}
|
||||
variant="plain"
|
||||
>
|
||||
<TimesIcon />
|
||||
</Button>
|
||||
@ -132,4 +114,20 @@ function Toolbar({
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(Toolbar);
|
||||
VisualizerToolbar.propTypes = {
|
||||
keyShown: bool.isRequired,
|
||||
nodes: arrayOf(shape()),
|
||||
onClose: func.isRequired,
|
||||
onDeleteAllClick: func.isRequired,
|
||||
onKeyToggle: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
onToolsToggle: func.isRequired,
|
||||
template: shape().isRequired,
|
||||
toolsShown: bool.isRequired,
|
||||
};
|
||||
|
||||
VisualizerToolbar.defaultProps = {
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
export default withI18n()(VisualizerToolbar);
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
export { default as Visualizer } from './Visualizer';
|
||||
export { default as VisualizerToolbar } from './VisualizerToolbar';
|
||||
export { default as VisualizerGraph } from './VisualizerGraph';
|
||||
export { default as VisualizerStartScreen } from './VisualizerStartScreen';
|
||||
export { default as VisualizerStartNode } from './VisualizerStartNode';
|
||||
export { default as VisualizerLink } from './VisualizerLink';
|
||||
export { default as VisualizerNode } from './VisualizerNode';
|
||||
export { default as VisualizerKey } from './VisualizerKey';
|
||||
export { default as VisualizerTools } from './VisualizerTools';
|
||||
export { default as VisualizerStartNode } from './VisualizerStartNode';
|
||||
export { default as VisualizerStartScreen } from './VisualizerStartScreen';
|
||||
export { default as VisualizerToolbar } from './VisualizerToolbar';
|
||||
|
||||
@ -18,8 +18,8 @@ export function calcZoomAndFit(gRef, svgRef) {
|
||||
.node()
|
||||
.getBoundingClientRect();
|
||||
|
||||
gBoundingClientRect.height = gBoundingClientRect.height / currentScale;
|
||||
gBoundingClientRect.width = gBoundingClientRect.width / currentScale;
|
||||
gBoundingClientRect.height /= currentScale;
|
||||
gBoundingClientRect.width /= currentScale;
|
||||
|
||||
const gBBoxDimensions = d3
|
||||
.select(gRef)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user