mirror of
https://github.com/ansible/awx.git
synced 2026-03-09 05:29:26 -02: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:
@@ -2,10 +2,10 @@ import React, { Fragment } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import SelectableCard from '@components/SelectableCard';
|
||||||
|
import Wizard from '@components/Wizard';
|
||||||
import SelectResourceStep from './SelectResourceStep';
|
import SelectResourceStep from './SelectResourceStep';
|
||||||
import SelectRoleStep from './SelectRoleStep';
|
import SelectRoleStep from './SelectRoleStep';
|
||||||
import { SelectableCard } from '@components/SelectableCard';
|
|
||||||
import { Wizard } from '@components/Wizard';
|
|
||||||
import { TeamsAPI, UsersAPI } from '../../api';
|
import { TeamsAPI, UsersAPI } from '../../api';
|
||||||
|
|
||||||
const readUsers = async queryParams =>
|
const readUsers = async queryParams =>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
alwaysVisible={false}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
label="Name"
|
label="Name"
|
||||||
value="jane brown"
|
value="jane brown"
|
||||||
@@ -83,6 +84,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
alwaysVisible={false}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
label="Team Roles"
|
label="Team Roles"
|
||||||
value={
|
value={
|
||||||
@@ -133,6 +135,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
alwaysVisible={false}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
label="Name"
|
label="Name"
|
||||||
value="jane brown"
|
value="jane brown"
|
||||||
@@ -144,6 +147,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
alwaysVisible={false}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
label="Team Roles"
|
label="Team Roles"
|
||||||
value={
|
value={
|
||||||
@@ -217,6 +221,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
alwaysVisible={false}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
label="Name"
|
label="Name"
|
||||||
value="jane brown"
|
value="jane brown"
|
||||||
@@ -228,6 +233,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
stacked={true}
|
stacked={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
alwaysVisible={false}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
label="Team Roles"
|
label="Team Roles"
|
||||||
value={
|
value={
|
||||||
@@ -400,6 +406,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
data-pf-content={true}
|
data-pf-content={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
alwaysVisible={false}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
label="Name"
|
label="Name"
|
||||||
value="jane brown"
|
value="jane brown"
|
||||||
@@ -573,6 +580,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
data-pf-content={true}
|
data-pf-content={true}
|
||||||
>
|
>
|
||||||
<Detail
|
<Detail
|
||||||
|
alwaysVisible={false}
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
label="Team Roles"
|
label="Team Roles"
|
||||||
value={
|
value={
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ function SelectableCard({ label, description, onClick, isSelected, dataCy }) {
|
|||||||
|
|
||||||
SelectableCard.propTypes = {
|
SelectableCard.propTypes = {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
isSelected: PropTypes.bool,
|
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', () => {
|
describe('Wizard', () => {
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mount(<Wizard />);
|
const wrapper = mount(
|
||||||
expect(wrapper).toMatchSnapshot();
|
<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 React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { node, number } from 'prop-types';
|
||||||
|
|
||||||
const TooltipContents = styled.div`
|
const TooltipContents = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -10,32 +11,32 @@ const TooltipArrows = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const TooltipArrowOuter = 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;
|
position: absolute;
|
||||||
top: calc(50% - 10px);
|
top: calc(50% - 10px);
|
||||||
width: 0;
|
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`
|
const TooltipArrowInner = styled.div`
|
||||||
position: absolute;
|
border-bottom: 10px solid transparent;
|
||||||
top: calc(50% - 10px);
|
|
||||||
left: 2px;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-right: 10px solid white;
|
border-right: 10px solid white;
|
||||||
border-top: 10px solid transparent;
|
border-top: 10px solid transparent;
|
||||||
border-bottom: 10px solid transparent;
|
height: 0;
|
||||||
|
left: 2px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - 10px);
|
||||||
|
width: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TooltipActions = styled.div`
|
const TooltipActions = styled.div`
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #c4c4c4;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
border: 1px solid #c4c4c4;
|
||||||
padding: 5px;
|
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;
|
export default WorkflowActionTooltip;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { func } from 'prop-types';
|
||||||
|
|
||||||
const TooltipItem = styled.div`
|
const TooltipItem = styled.div`
|
||||||
height: 25px;
|
|
||||||
width: 25px;
|
|
||||||
font-size: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 25px;
|
||||||
|
justify-content: center;
|
||||||
|
width: 25px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: white;
|
color: white;
|
||||||
@@ -21,21 +22,33 @@ const TooltipItem = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowActionTooltip({
|
function WorkflowActionTooltipItem({
|
||||||
children,
|
children,
|
||||||
|
onClick,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
onClick,
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TooltipItem
|
<TooltipItem
|
||||||
|
onClick={onClick}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
onClick={onClick}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</TooltipItem>
|
</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';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const Outer = styled.div`
|
const Outer = styled.div`
|
||||||
position: relative;
|
|
||||||
height: 0;
|
height: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Inner = styled.div`
|
const Inner = styled.div`
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: 10px;
|
|
||||||
background-color: #383f44;
|
background-color: #383f44;
|
||||||
color: white;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
color: white;
|
||||||
|
left: 10px;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowHelp({ children }) {
|
function WorkflowHelp({ children }) {
|
||||||
|
|||||||
@@ -5,50 +5,50 @@ import styled from 'styled-components';
|
|||||||
import { ExclamationTriangleIcon, PauseIcon } from '@patternfly/react-icons';
|
import { ExclamationTriangleIcon, PauseIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
border: 1px solid #c7c7c7;
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
min-width: 100px;
|
border: 1px solid #c7c7c7;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
min-width: 100px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Header = styled.div`
|
const Header = styled.div`
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid #c7c7c7;
|
border-bottom: 1px solid #c7c7c7;
|
||||||
|
padding: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Key = styled.ul`
|
const Key = styled.ul`
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
padding: 5px 0px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 0px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NodeTypeLetter = styled.div`
|
const NodeTypeLetter = styled.div`
|
||||||
font-size: 10px;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 20px;
|
|
||||||
background-color: #393f43;
|
background-color: #393f43;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
line-height: 20px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledExclamationTriangleIcon = styled(ExclamationTriangleIcon)`
|
const StyledExclamationTriangleIcon = styled(ExclamationTriangleIcon)`
|
||||||
color: #f0ad4d;
|
color: #f0ad4d;
|
||||||
margin-right: 10px;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Link = styled.div`
|
const Link = styled.div`
|
||||||
height: 5px;
|
height: 5px;
|
||||||
width: 20px;
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
width: 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SuccessLink = styled(Link)`
|
const SuccessLink = styled(Link)`
|
||||||
@@ -63,7 +63,7 @@ const AlwaysLink = styled(Link)`
|
|||||||
background-color: #337ab7;
|
background-color: #337ab7;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerKey({ i18n }) {
|
function WorkflowKey({ i18n }) {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Header>
|
<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 { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { shape } from 'prop-types';
|
||||||
|
|
||||||
const GridDL = styled.dl`
|
const GridDL = styled.dl`
|
||||||
|
column-gap: 15px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content;
|
grid-template-columns: max-content;
|
||||||
column-gap: 15px;
|
|
||||||
row-gap: 0px;
|
row-gap: 0px;
|
||||||
dt {
|
dt {
|
||||||
grid-column-start: 1;
|
grid-column-start: 1;
|
||||||
@@ -18,7 +19,7 @@ const GridDL = styled.dl`
|
|||||||
|
|
||||||
function WorkflowLinkHelp({ link, i18n }) {
|
function WorkflowLinkHelp({ link, i18n }) {
|
||||||
let linkType;
|
let linkType;
|
||||||
switch (link.edgeType) {
|
switch (link.linkType) {
|
||||||
case 'always':
|
case 'always':
|
||||||
linkType = i18n._(t`Always`);
|
linkType = i18n._(t`Always`);
|
||||||
break;
|
break;
|
||||||
@@ -42,4 +43,8 @@ function WorkflowLinkHelp({ link, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkflowLinkHelp.propTypes = {
|
||||||
|
link: shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(WorkflowLinkHelp);
|
export default withI18n()(WorkflowLinkHelp);
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import React, { Fragment } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { shape } from 'prop-types';
|
||||||
import { secondsToHHMMSS } from '@util/dates';
|
import { secondsToHHMMSS } from '@util/dates';
|
||||||
|
|
||||||
const GridDL = styled.dl`
|
const GridDL = styled.dl`
|
||||||
|
column-gap: 15px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content;
|
grid-template-columns: max-content;
|
||||||
column-gap: 15px;
|
|
||||||
row-gap: 0px;
|
row-gap: 0px;
|
||||||
dt {
|
dt {
|
||||||
grid-column-start: 1;
|
grid-column-start: 1;
|
||||||
@@ -134,4 +135,8 @@ function WorkflowNodeHelp({ node, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkflowNodeHelp.propTypes = {
|
||||||
|
node: shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(WorkflowNodeHelp);
|
export default withI18n()(WorkflowNodeHelp);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { shape } from 'prop-types';
|
||||||
import { PauseIcon } from '@patternfly/react-icons';
|
import { PauseIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
const NodeTypeLetter = styled.foreignObject`
|
const NodeTypeLetter = styled.foreignObject`
|
||||||
font-size: 10px;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 20px;
|
|
||||||
background-color: #393f43;
|
background-color: #393f43;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowNodeTypeLetter({ node }) {
|
function WorkflowNodeTypeLetter({ node }) {
|
||||||
@@ -52,4 +53,8 @@ function WorkflowNodeTypeLetter({ node }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkflowNodeTypeLetter.propTypes = {
|
||||||
|
node: shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default WorkflowNodeTypeLetter;
|
export default WorkflowNodeTypeLetter;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { func, number } from 'prop-types';
|
||||||
import { Tooltip } from '@patternfly/react-core';
|
import { Tooltip } from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
CaretDownIcon,
|
CaretDownIcon,
|
||||||
@@ -15,19 +16,19 @@ import {
|
|||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
border: 1px solid #c7c7c7;
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
border: 1px solid #c7c7c7;
|
||||||
height: 135px;
|
height: 135px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Header = styled.div`
|
const Header = styled.div`
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid #c7c7c7;
|
border-bottom: 1px solid #c7c7c7;
|
||||||
|
padding: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Pan = styled.div`
|
const Pan = styled.div`
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const PanCenter = styled.div`
|
const PanCenter = styled.div`
|
||||||
@@ -36,18 +37,18 @@ const PanCenter = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Tools = styled.div`
|
const Tools = styled.div`
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerTools({
|
function WorkflowTools({
|
||||||
i18n,
|
i18n,
|
||||||
zoomPercentage,
|
|
||||||
onZoomChange,
|
|
||||||
onFitGraph,
|
onFitGraph,
|
||||||
onPan,
|
onPan,
|
||||||
onPanToMiddle,
|
onPanToMiddle,
|
||||||
|
onZoomChange,
|
||||||
|
zoomPercentage,
|
||||||
}) {
|
}) {
|
||||||
const zoomIn = () => {
|
const zoomIn = () => {
|
||||||
const newScale =
|
const newScale =
|
||||||
@@ -81,14 +82,16 @@ function VisualizerTools({
|
|||||||
<MinusIcon onClick={() => zoomOut()} css="margin-right: 10px;" />
|
<MinusIcon onClick={() => zoomOut()} css="margin-right: 10px;" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<input
|
<input
|
||||||
type="range"
|
|
||||||
id="zoom-slider"
|
id="zoom-slider"
|
||||||
value={zoomPercentage}
|
|
||||||
min="10"
|
|
||||||
max="200"
|
max="200"
|
||||||
|
min="10"
|
||||||
|
onChange={event =>
|
||||||
|
onZoomChange(parseInt(event.target.value, 10) / 100)
|
||||||
|
}
|
||||||
step="10"
|
step="10"
|
||||||
onChange={event => onZoomChange(parseInt(event.target.value) / 100)}
|
type="range"
|
||||||
></input>
|
value={zoomPercentage}
|
||||||
|
/>
|
||||||
<Tooltip content={i18n._(t`Zoom In`)} position="bottom">
|
<Tooltip content={i18n._(t`Zoom In`)} position="bottom">
|
||||||
<PlusIcon onClick={() => zoomIn()} css="margin: 0px 25px 0px 10px;" />
|
<PlusIcon onClick={() => zoomIn()} css="margin: 0px 25px 0px 10px;" />
|
||||||
</Tooltip>
|
</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 WorkflowActionTooltip } from './WorkflowActionTooltip';
|
||||||
export {
|
export {
|
||||||
default as WorkflowActionTooltipItem,
|
default as WorkflowActionTooltipItem,
|
||||||
} from './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,
|
InventoriesAPI,
|
||||||
AdHocCommandsAPI,
|
AdHocCommandsAPI,
|
||||||
} from '@api';
|
} 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)`
|
const VariablesInput = styled(_VariablesInput)`
|
||||||
.pf-c-form__label {
|
.pf-c-form__label {
|
||||||
|
|||||||
@@ -2,64 +2,60 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { shape } from 'prop-types';
|
||||||
import { CardBody as PFCardBody } from '@patternfly/react-core';
|
import { CardBody as PFCardBody } from '@patternfly/react-core';
|
||||||
import { layoutGraph } from '@util/workflow';
|
import { layoutGraph } from '@util/workflow';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import { WorkflowJobsAPI } from '@api';
|
import { WorkflowJobsAPI } from '@api';
|
||||||
import WorkflowOutputGraph from './WorkflowOutputGraph';
|
import WorkflowOutputGraph from './WorkflowOutputGraph';
|
||||||
|
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
||||||
|
|
||||||
const CardBody = styled(PFCardBody)`
|
const CardBody = styled(PFCardBody)`
|
||||||
height: calc(100vh - 240px);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
height: calc(100vh - 240px);
|
||||||
|
|
||||||
const Toolbar = styled.div`
|
|
||||||
height: 50px;
|
|
||||||
background-color: grey;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const fetchWorkflowNodes = async (jobId, pageNo = 1, nodes = []) => {
|
const fetchWorkflowNodes = async (jobId, pageNo = 1, nodes = []) => {
|
||||||
try {
|
const { data } = await WorkflowJobsAPI.readNodes(jobId, {
|
||||||
const { data } = await WorkflowJobsAPI.readNodes(jobId, {
|
page_size: 200,
|
||||||
page_size: 200,
|
page: pageNo,
|
||||||
page: pageNo,
|
});
|
||||||
});
|
if (data.next) {
|
||||||
if (data.next) {
|
return fetchWorkflowNodes(
|
||||||
return await fetchWorkflowNodes(
|
jobId,
|
||||||
jobId,
|
pageNo + 1,
|
||||||
pageNo + 1,
|
nodes.concat(data.results)
|
||||||
nodes.concat(data.results)
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
return nodes.concat(data.results);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
return nodes.concat(data.results);
|
||||||
};
|
};
|
||||||
|
|
||||||
function WorkflowOutput({ job, i18n }) {
|
function WorkflowOutput({ job, i18n }) {
|
||||||
const [contentError, setContentError] = useState(null);
|
const [contentError, setContentError] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [graphLinks, setGraphLinks] = useState([]);
|
const [graphLinks, setGraphLinks] = useState([]);
|
||||||
const [graphNodes, setGraphNodes] = useState([]);
|
const [graphNodes, setGraphNodes] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [nodePositions, setNodePositions] = useState(null);
|
const [nodePositions, setNodePositions] = useState(null);
|
||||||
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
const [showTools, setShowTools] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildGraphArrays = nodes => {
|
const buildGraphArrays = nodes => {
|
||||||
const nonRootNodeIds = [];
|
|
||||||
const allNodeIds = [];
|
const allNodeIds = [];
|
||||||
const arrayOfLinksForChart = [];
|
const arrayOfLinksForChart = [];
|
||||||
const nodeIdToChartNodeIdMapping = {};
|
|
||||||
const chartNodeIdToIndexMapping = {};
|
const chartNodeIdToIndexMapping = {};
|
||||||
|
const nodeIdToChartNodeIdMapping = {};
|
||||||
const nodeRef = {};
|
const nodeRef = {};
|
||||||
|
const nonRootNodeIds = [];
|
||||||
let nodeIdCounter = 1;
|
let nodeIdCounter = 1;
|
||||||
const arrayOfNodesForChart = [
|
const arrayOfNodesForChart = [
|
||||||
{
|
{
|
||||||
@@ -110,7 +106,7 @@ function WorkflowOutput({ job, i18n }) {
|
|||||||
arrayOfLinksForChart.push({
|
arrayOfLinksForChart.push({
|
||||||
source: arrayOfNodesForChart[sourceIndex],
|
source: arrayOfNodesForChart[sourceIndex],
|
||||||
target: arrayOfNodesForChart[targetIndex],
|
target: arrayOfNodesForChart[targetIndex],
|
||||||
edgeType: 'success',
|
linkType: 'success',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
nonRootNodeIds.push(nodeId);
|
nonRootNodeIds.push(nodeId);
|
||||||
@@ -121,7 +117,7 @@ function WorkflowOutput({ job, i18n }) {
|
|||||||
arrayOfLinksForChart.push({
|
arrayOfLinksForChart.push({
|
||||||
source: arrayOfNodesForChart[sourceIndex],
|
source: arrayOfNodesForChart[sourceIndex],
|
||||||
target: arrayOfNodesForChart[targetIndex],
|
target: arrayOfNodesForChart[targetIndex],
|
||||||
edgeType: 'failure',
|
linkType: 'failure',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
nonRootNodeIds.push(nodeId);
|
nonRootNodeIds.push(nodeId);
|
||||||
@@ -132,7 +128,7 @@ function WorkflowOutput({ job, i18n }) {
|
|||||||
arrayOfLinksForChart.push({
|
arrayOfLinksForChart.push({
|
||||||
source: arrayOfNodesForChart[sourceIndex],
|
source: arrayOfNodesForChart[sourceIndex],
|
||||||
target: arrayOfNodesForChart[targetIndex],
|
target: arrayOfNodesForChart[targetIndex],
|
||||||
edgeType: 'always',
|
linkType: 'always',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
nonRootNodeIds.push(nodeId);
|
nonRootNodeIds.push(nodeId);
|
||||||
@@ -151,7 +147,7 @@ function WorkflowOutput({ job, i18n }) {
|
|||||||
arrayOfLinksForChart.push({
|
arrayOfLinksForChart.push({
|
||||||
source: arrayOfNodesForChart[0],
|
source: arrayOfNodesForChart[0],
|
||||||
target: arrayOfNodesForChart[targetIndex],
|
target: arrayOfNodesForChart[targetIndex],
|
||||||
edgeType: 'always',
|
linkType: 'always',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -206,12 +202,21 @@ function WorkflowOutput({ job, i18n }) {
|
|||||||
return (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Toolbar>Toolbar</Toolbar>
|
<WorkflowOutputToolbar
|
||||||
|
job={job}
|
||||||
|
keyShown={showKey}
|
||||||
|
nodes={graphNodes}
|
||||||
|
onKeyToggle={() => setShowKey(!showKey)}
|
||||||
|
onToolsToggle={() => setShowTools(!showTools)}
|
||||||
|
toolsShown={showTools}
|
||||||
|
/>
|
||||||
{nodePositions && (
|
{nodePositions && (
|
||||||
<WorkflowOutputGraph
|
<WorkflowOutputGraph
|
||||||
links={graphLinks}
|
links={graphLinks}
|
||||||
nodes={graphNodes}
|
|
||||||
nodePositions={nodePositions}
|
nodePositions={nodePositions}
|
||||||
|
nodes={graphNodes}
|
||||||
|
showKey={showKey}
|
||||||
|
showTools={showTools}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
@@ -219,4 +224,8 @@ function WorkflowOutput({ job, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkflowOutput.propTypes = {
|
||||||
|
job: shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(WorkflowOutput);
|
export default withI18n()(WorkflowOutput);
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import { WorkflowHelp, WorkflowNodeHelp } from '@components/Workflow';
|
import { arrayOf, bool, shape } from 'prop-types';
|
||||||
import { calcZoomAndFit } from '@util/workflow';
|
import { calcZoomAndFit, getZoomTranslate } from '@util/workflow';
|
||||||
import {
|
import {
|
||||||
WorkflowOutputLink,
|
WorkflowOutputLink,
|
||||||
WorkflowOutputNode,
|
WorkflowOutputNode,
|
||||||
WorkflowOutputStartNode,
|
WorkflowOutputStartNode,
|
||||||
} from '@screens/Job/WorkflowOutput';
|
} 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 [nodeHelp, setNodeHelp] = useState();
|
||||||
|
const [zoomPercentage, setZoomPercentage] = useState(100);
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const gRef = useRef(null);
|
const gRef = useRef(null);
|
||||||
|
|
||||||
@@ -20,6 +33,75 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
|||||||
'transform',
|
'transform',
|
||||||
`translate(${translation}) scale(${d3.event.transform.k})`
|
`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
|
const zoomRef = d3
|
||||||
@@ -34,12 +116,17 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
|||||||
|
|
||||||
// Attempt to zoom the graph to fit the available screen space
|
// Attempt to zoom the graph to fit the available screen space
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const [scaleToFit, yTranslate] = calcZoomAndFit(gRef.current);
|
const [scaleToFit, yTranslate] = calcZoomAndFit(
|
||||||
|
gRef.current,
|
||||||
|
svgRef.current
|
||||||
|
);
|
||||||
|
|
||||||
d3.select(svgRef.current).call(
|
d3.select(svgRef.current).call(
|
||||||
zoomRef.transform,
|
zoomRef.transform,
|
||||||
d3.zoomIdentity.translate(0, yTranslate).scale(scaleToFit)
|
d3.zoomIdentity.translate(0, yTranslate).scale(scaleToFit)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setZoomPercentage(scaleToFit * 100);
|
||||||
// We only want this to run once (when the component mounts)
|
// We only want this to run once (when the component mounts)
|
||||||
// Including zoomRef.transform in the deps array will cause this to
|
// Including zoomRef.transform in the deps array will cause this to
|
||||||
// run very frequently.
|
// run very frequently.
|
||||||
@@ -78,10 +165,10 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
|||||||
return (
|
return (
|
||||||
<WorkflowOutputNode
|
<WorkflowOutputNode
|
||||||
key={`node-${node.id}`}
|
key={`node-${node.id}`}
|
||||||
node={node}
|
|
||||||
nodePositions={nodePositions}
|
|
||||||
mouseEnter={() => setNodeHelp(node)}
|
mouseEnter={() => setNodeHelp(node)}
|
||||||
mouseLeave={() => setNodeHelp(null)}
|
mouseLeave={() => setNodeHelp(null)}
|
||||||
|
node={node}
|
||||||
|
nodePositions={nodePositions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -90,8 +177,28 @@ function WorkflowOutputGraph({ links, nodes, nodePositions }) {
|
|||||||
]}
|
]}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</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>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkflowOutputGraph.propTypes = {
|
||||||
|
links: arrayOf(shape()).isRequired,
|
||||||
|
nodePositions: shape().isRequired,
|
||||||
|
nodes: arrayOf(shape()).isRequired,
|
||||||
|
showKey: bool.isRequired,
|
||||||
|
showTools: bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default WorkflowOutputGraph;
|
export default WorkflowOutputGraph;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { shape } from 'prop-types';
|
||||||
import { generateLine, getLinePoints } from '@util/workflow';
|
import { generateLine, getLinePoints } from '@util/workflow';
|
||||||
|
|
||||||
function WorkflowOutputLink({ link, nodePositions }) {
|
function WorkflowOutputLink({ link, nodePositions }) {
|
||||||
@@ -6,16 +7,16 @@ function WorkflowOutputLink({ link, nodePositions }) {
|
|||||||
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
const [pathStroke, setPathStroke] = useState('#CCCCCC');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (link.edgeType === 'failure') {
|
if (link.linkType === 'failure') {
|
||||||
setPathStroke('#d9534f');
|
setPathStroke('#d9534f');
|
||||||
}
|
}
|
||||||
if (link.edgeType === 'success') {
|
if (link.linkType === 'success') {
|
||||||
setPathStroke('#5cb85c');
|
setPathStroke('#5cb85c');
|
||||||
}
|
}
|
||||||
if (link.edgeType === 'always') {
|
if (link.linkType === 'always') {
|
||||||
setPathStroke('#337ab7');
|
setPathStroke('#337ab7');
|
||||||
}
|
}
|
||||||
}, [link.edgeType]);
|
}, [link.linkType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const linePoints = getLinePoints(link, nodePositions);
|
const linePoints = getLinePoints(link, nodePositions);
|
||||||
@@ -37,4 +38,9 @@ function WorkflowOutputLink({ link, nodePositions }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkflowOutputLink.propTypes = {
|
||||||
|
link: shape().isRequired,
|
||||||
|
nodePositions: shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default WorkflowOutputLink;
|
export default WorkflowOutputLink;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { func, shape } from 'prop-types';
|
||||||
import { StatusIcon } from '@components/Sparkline';
|
import { StatusIcon } from '@components/Sparkline';
|
||||||
import { WorkflowNodeTypeLetter } from '@components/Workflow';
|
import { WorkflowNodeTypeLetter } from '@components/Workflow';
|
||||||
import { secondsToHHMMSS } from '@util/dates';
|
import { secondsToHHMMSS } from '@util/dates';
|
||||||
import { JOB_TYPE_URL_SEGMENTS } from '@constants';
|
|
||||||
import { constants as wfConstants } from '@util/workflow';
|
import { constants as wfConstants } from '@util/workflow';
|
||||||
|
|
||||||
const NodeG = styled.g`
|
const NodeG = styled.g`
|
||||||
@@ -13,12 +14,12 @@ const NodeG = styled.g`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const JobTopLine = styled.div`
|
const JobTopLine = styled.div`
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
@@ -29,8 +30,8 @@ const JobTopLine = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Elapsed = styled.div`
|
const Elapsed = styled.div`
|
||||||
text-align: center;
|
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -48,18 +49,19 @@ const NodeContents = styled.foreignObject`
|
|||||||
|
|
||||||
const NodeDefaultLabel = styled.p`
|
const NodeDefaultLabel = styled.p`
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function WorkflowOutputNode({
|
function WorkflowOutputNode({
|
||||||
node,
|
history,
|
||||||
nodePositions,
|
i18n,
|
||||||
mouseEnter,
|
mouseEnter,
|
||||||
mouseLeave,
|
mouseLeave,
|
||||||
i18n,
|
node,
|
||||||
|
nodePositions,
|
||||||
}) {
|
}) {
|
||||||
let borderColor = '#93969A';
|
let borderColor = '#93969A';
|
||||||
|
|
||||||
@@ -78,10 +80,7 @@ function WorkflowOutputNode({
|
|||||||
|
|
||||||
const handleNodeClick = () => {
|
const handleNodeClick = () => {
|
||||||
if (node.job) {
|
if (node.job) {
|
||||||
window.open(
|
history.push(`/jobs/${node.job.id}/details`);
|
||||||
`/#/jobs/${JOB_TYPE_URL_SEGMENTS[node.job.type]}/${node.job.id}`,
|
|
||||||
'_blank'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,13 +95,13 @@ function WorkflowOutputNode({
|
|||||||
onMouseLeave={mouseLeave}
|
onMouseLeave={mouseLeave}
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
width={wfConstants.nodeW}
|
fill="#FFFFFF"
|
||||||
height={wfConstants.nodeH}
|
height={wfConstants.nodeH}
|
||||||
rx="2"
|
rx="2"
|
||||||
ry="2"
|
ry="2"
|
||||||
stroke={borderColor}
|
stroke={borderColor}
|
||||||
strokeWidth="2px"
|
strokeWidth="2px"
|
||||||
fill="#FFFFFF"
|
width={wfConstants.nodeW}
|
||||||
/>
|
/>
|
||||||
<NodeContents height="60" width="180">
|
<NodeContents height="60" width="180">
|
||||||
{node.job ? (
|
{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 React from 'react';
|
||||||
|
import { shape } from 'prop-types';
|
||||||
import { constants as wfConstants } from '@util/workflow';
|
import { constants as wfConstants } from '@util/workflow';
|
||||||
|
|
||||||
function WorkflowOutputStartNode({ nodePositions }) {
|
function WorkflowOutputStartNode({ nodePositions }) {
|
||||||
return (
|
return (
|
||||||
<g id="node-1" transform={`translate(${nodePositions[1].x},0)`}>
|
<g id="node-1" transform={`translate(${nodePositions[1].x},0)`}>
|
||||||
<rect
|
<rect
|
||||||
width={wfConstants.rootW}
|
fill="#0279BC"
|
||||||
height={wfConstants.rootH}
|
height={wfConstants.rootH}
|
||||||
y="10"
|
|
||||||
rx="2"
|
rx="2"
|
||||||
ry="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">
|
<text x="13" y="30" dy=".35em" fill="white">
|
||||||
START
|
START
|
||||||
</text>
|
</text>
|
||||||
@@ -20,4 +21,8 @@ function WorkflowOutputStartNode({ nodePositions }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorkflowOutputStartNode.propTypes = {
|
||||||
|
nodePositions: shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default WorkflowOutputStartNode;
|
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 WorkflowOutputLink } from './WorkflowOutputLink';
|
||||||
export { default as WorkflowOutputNode } from './WorkflowOutputNode';
|
export { default as WorkflowOutputNode } from './WorkflowOutputNode';
|
||||||
export { default as WorkflowOutputStartNode } from './WorkflowOutputStartNode';
|
export { default as WorkflowOutputStartNode } from './WorkflowOutputStartNode';
|
||||||
|
export { default as WorkflowOutputToolbar } from './WorkflowOutputToolbar';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import Templates from './Templates';
|
import Templates from './Templates';
|
||||||
|
|||||||
@@ -2,15 +2,12 @@ import React from 'react';
|
|||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func } from 'prop-types';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
|
|
||||||
function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
variant="danger"
|
|
||||||
title={i18n._(t`Remove All Nodes`)}
|
|
||||||
isOpen={true}
|
|
||||||
onClose={onCancel}
|
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
key="remove"
|
key="remove"
|
||||||
@@ -29,6 +26,10 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
|||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
|
isOpen
|
||||||
|
onClose={onCancel}
|
||||||
|
title={i18n._(t`Remove All Nodes`)}
|
||||||
|
variant="danger"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
{i18n._(
|
{i18n._(
|
||||||
@@ -39,4 +40,9 @@ function DeleteAllNodesModal({ i18n, onConfirm, onCancel }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DeleteAllNodesModal.propTypes = {
|
||||||
|
onCancel: func.isRequired,
|
||||||
|
onConfirm: func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(DeleteAllNodesModal);
|
export default withI18n()(DeleteAllNodesModal);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
|
|||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func, shape } from 'prop-types';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
|
|
||||||
function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
||||||
@@ -13,18 +14,18 @@ function LinkDeleteModal({ i18n, linkToDelete, onConfirm, onCancel }) {
|
|||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
key="remove"
|
|
||||||
variant="danger"
|
|
||||||
aria-label={i18n._(t`Confirm link removal`)}
|
aria-label={i18n._(t`Confirm link removal`)}
|
||||||
|
key="remove"
|
||||||
onClick={() => onConfirm()}
|
onClick={() => onConfirm()}
|
||||||
|
variant="danger"
|
||||||
>
|
>
|
||||||
{i18n._(t`Remove`)}
|
{i18n._(t`Remove`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
key="cancel"
|
|
||||||
variant="secondary"
|
|
||||||
aria-label={i18n._(t`Cancel link removal`)}
|
aria-label={i18n._(t`Cancel link removal`)}
|
||||||
|
key="cancel"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>,
|
</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);
|
export default withI18n()(LinkDeleteModal);
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
import React, { useState } from 'react';
|
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 { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { FormGroup } from '@patternfly/react-core';
|
import { func, node, string } from 'prop-types';
|
||||||
import AnsibleSelect from '@components/AnsibleSelect';
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
|
|
||||||
function LinkModal({
|
function LinkModal({ linkType, header, i18n, onCancel, onConfirm }) {
|
||||||
i18n,
|
const [newLinkType, setNewLinkType] = useState(linkType);
|
||||||
header,
|
|
||||||
onCancel,
|
|
||||||
onConfirm,
|
|
||||||
edgeType = 'success',
|
|
||||||
}) {
|
|
||||||
const [newEdgeType, setNewEdgeType] = useState(edgeType);
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
width={600}
|
width={600}
|
||||||
header={header}
|
header={header}
|
||||||
isOpen={true}
|
isOpen
|
||||||
title={i18n._(t`Workflow Link`)}
|
title={i18n._(t`Workflow Link`)}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
actions={[
|
actions={[
|
||||||
@@ -25,7 +19,7 @@ function LinkModal({
|
|||||||
key="save"
|
key="save"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
aria-label={i18n._(t`Save link changes`)}
|
aria-label={i18n._(t`Save link changes`)}
|
||||||
onClick={() => onConfirm(newEdgeType)}
|
onClick={() => onConfirm(newLinkType)}
|
||||||
>
|
>
|
||||||
{i18n._(t`Save`)}
|
{i18n._(t`Save`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
@@ -42,7 +36,7 @@ function LinkModal({
|
|||||||
<FormGroup fieldId="link-select" label={i18n._(t`Run`)}>
|
<FormGroup fieldId="link-select" label={i18n._(t`Run`)}>
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
id="link-select"
|
id="link-select"
|
||||||
value={newEdgeType}
|
value={newLinkType}
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
value: 'always',
|
value: 'always',
|
||||||
@@ -61,7 +55,7 @@ function LinkModal({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(event, value) => {
|
onChange={(event, value) => {
|
||||||
setNewEdgeType(value);
|
setNewLinkType(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</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);
|
export default withI18n()(LinkModal);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
|
|||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func, shape } from 'prop-types';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
|
|
||||||
function NodeDeleteModal({ i18n, nodeToDelete, onConfirm, onCancel }) {
|
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);
|
export default withI18n()(NodeDeleteModal);
|
||||||
|
|||||||
@@ -2,82 +2,84 @@ import React, { useState } from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { bool, func, node, shape } from 'prop-types';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
WizardContextConsumer,
|
WizardContextConsumer,
|
||||||
WizardFooter,
|
WizardFooter,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import NodeTypeStep from './NodeTypeStep/NodeTypeStep';
|
import Wizard from '@components/Wizard';
|
||||||
import RunStep from './RunStep';
|
import { NodeTypeStep } from './NodeTypeStep';
|
||||||
import NodeNextButton from './NodeNextButton';
|
import { RunStep, NodeNextButton } from '.';
|
||||||
import { Wizard } from '@components/Wizard';
|
|
||||||
|
|
||||||
function NodeModal({
|
function NodeModal({
|
||||||
|
askLinkType,
|
||||||
history,
|
history,
|
||||||
i18n,
|
i18n,
|
||||||
title,
|
nodeToEdit,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
node,
|
title,
|
||||||
askLinkType,
|
|
||||||
}) {
|
}) {
|
||||||
let defaultNodeType = 'job_template';
|
|
||||||
let defaultNodeResource = null;
|
|
||||||
let defaultApprovalName = '';
|
|
||||||
let defaultApprovalDescription = '';
|
let defaultApprovalDescription = '';
|
||||||
|
let defaultApprovalName = '';
|
||||||
let defaultApprovalTimeout = 0;
|
let defaultApprovalTimeout = 0;
|
||||||
if (node && node.unifiedJobTemplate) {
|
let defaultNodeResource = null;
|
||||||
|
let defaultNodeType = 'job_template';
|
||||||
|
if (nodeToEdit && nodeToEdit.unifiedJobTemplate) {
|
||||||
if (
|
if (
|
||||||
node &&
|
nodeToEdit &&
|
||||||
node.unifiedJobTemplate &&
|
nodeToEdit.unifiedJobTemplate &&
|
||||||
(node.unifiedJobTemplate.type || node.unifiedJobTemplate.unified_job_type)
|
(nodeToEdit.unifiedJobTemplate.type ||
|
||||||
|
nodeToEdit.unifiedJobTemplate.unified_job_type)
|
||||||
) {
|
) {
|
||||||
const ujtType =
|
const ujtType =
|
||||||
node.unifiedJobTemplate.type ||
|
nodeToEdit.unifiedJobTemplate.type ||
|
||||||
node.unifiedJobTemplate.unified_job_type;
|
nodeToEdit.unifiedJobTemplate.unified_job_type;
|
||||||
switch (ujtType) {
|
switch (ujtType) {
|
||||||
case 'job_template':
|
case 'job_template':
|
||||||
case 'job':
|
case 'job':
|
||||||
defaultNodeType = 'job_template';
|
defaultNodeType = 'job_template';
|
||||||
defaultNodeResource = node.unifiedJobTemplate;
|
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
||||||
break;
|
break;
|
||||||
case 'project':
|
case 'project':
|
||||||
case 'project_update':
|
case 'project_update':
|
||||||
defaultNodeType = 'project_sync';
|
defaultNodeType = 'project_sync';
|
||||||
defaultNodeResource = node.unifiedJobTemplate;
|
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
||||||
break;
|
break;
|
||||||
case 'inventory_source':
|
case 'inventory_source':
|
||||||
case 'inventory_update':
|
case 'inventory_update':
|
||||||
defaultNodeType = 'inventory_source_sync';
|
defaultNodeType = 'inventory_source_sync';
|
||||||
defaultNodeResource = node.unifiedJobTemplate;
|
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
||||||
break;
|
break;
|
||||||
case 'workflow_job_template':
|
case 'workflow_job_template':
|
||||||
case 'workflow_job':
|
case 'workflow_job':
|
||||||
defaultNodeType = 'workflow_job_template';
|
defaultNodeType = 'workflow_job_template';
|
||||||
defaultNodeResource = node.unifiedJobTemplate;
|
defaultNodeResource = nodeToEdit.unifiedJobTemplate;
|
||||||
break;
|
break;
|
||||||
case 'workflow_approval_template':
|
case 'workflow_approval_template':
|
||||||
case 'workflow_approval':
|
case 'workflow_approval':
|
||||||
defaultNodeType = 'approval';
|
defaultNodeType = 'approval';
|
||||||
defaultApprovalName = node.unifiedJobTemplate.name;
|
defaultApprovalName = nodeToEdit.unifiedJobTemplate.name;
|
||||||
defaultApprovalDescription = node.unifiedJobTemplate.description;
|
defaultApprovalDescription =
|
||||||
defaultApprovalTimeout = node.unifiedJobTemplate.timeout;
|
nodeToEdit.unifiedJobTemplate.description;
|
||||||
|
defaultApprovalTimeout = nodeToEdit.unifiedJobTemplate.timeout;
|
||||||
break;
|
break;
|
||||||
default:
|
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(
|
const [approvalDescription, setApprovalDescription] = useState(
|
||||||
defaultApprovalDescription
|
defaultApprovalDescription
|
||||||
);
|
);
|
||||||
|
const [approvalName, setApprovalName] = useState(defaultApprovalName);
|
||||||
const [approvalTimeout, setApprovalTimeout] = useState(
|
const [approvalTimeout, setApprovalTimeout] = useState(
|
||||||
defaultApprovalTimeout
|
defaultApprovalTimeout
|
||||||
);
|
);
|
||||||
|
const [linkType, setLinkType] = useState('success');
|
||||||
|
const [nodeResource, setNodeResource] = useState(defaultNodeResource);
|
||||||
|
const [nodeType, setNodeType] = useState(defaultNodeType);
|
||||||
|
const [triggerNext, setTriggerNext] = useState(0);
|
||||||
|
|
||||||
const clearQueryParams = () => {
|
const clearQueryParams = () => {
|
||||||
const parts = history.location.search.replace(/^\?/, '').split('&');
|
const parts = history.location.search.replace(/^\?/, '').split('&');
|
||||||
@@ -95,19 +97,17 @@ function NodeModal({
|
|||||||
const resource =
|
const resource =
|
||||||
nodeType === 'approval'
|
nodeType === 'approval'
|
||||||
? {
|
? {
|
||||||
name: approvalName,
|
|
||||||
description: approvalDescription,
|
description: approvalDescription,
|
||||||
|
name: approvalName,
|
||||||
timeout: approvalTimeout,
|
timeout: approvalTimeout,
|
||||||
type: 'workflow_approval_template',
|
type: 'workflow_approval_template',
|
||||||
}
|
}
|
||||||
: nodeResource;
|
: nodeResource;
|
||||||
|
|
||||||
// TODO: pick edgeType or linkType and be consistent across all files.
|
|
||||||
|
|
||||||
onSave({
|
onSave({
|
||||||
nodeType,
|
linkType,
|
||||||
edgeType: linkType,
|
|
||||||
nodeResource: resource,
|
nodeResource: resource,
|
||||||
|
nodeType,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,15 +145,15 @@ function NodeModal({
|
|||||||
(nodeType === 'approval' && approvalName !== ''),
|
(nodeType === 'approval' && approvalName !== ''),
|
||||||
component: (
|
component: (
|
||||||
<NodeTypeStep
|
<NodeTypeStep
|
||||||
nodeType={nodeType}
|
|
||||||
updateNodeType={handleNodeTypeChange}
|
|
||||||
nodeResource={nodeResource}
|
|
||||||
updateNodeResource={setNodeResource}
|
|
||||||
name={approvalName}
|
|
||||||
updateName={setApprovalName}
|
|
||||||
description={approvalDescription}
|
description={approvalDescription}
|
||||||
updateDescription={setApprovalDescription}
|
name={approvalName}
|
||||||
|
nodeResource={nodeResource}
|
||||||
|
nodeType={nodeType}
|
||||||
timeout={approvalTimeout}
|
timeout={approvalTimeout}
|
||||||
|
updateDescription={setApprovalDescription}
|
||||||
|
updateName={setApprovalName}
|
||||||
|
updateNodeResource={setNodeResource}
|
||||||
|
updateNodeType={handleNodeTypeChange}
|
||||||
updateTimeout={setApprovalTimeout}
|
updateTimeout={setApprovalTimeout}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -198,15 +198,27 @@ function NodeModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Wizard
|
<Wizard
|
||||||
style={{ overflow: 'scroll' }}
|
footer={CustomFooter}
|
||||||
isOpen
|
isOpen
|
||||||
steps={steps}
|
|
||||||
title={wizardTitle}
|
|
||||||
onClose={handleCancel}
|
onClose={handleCancel}
|
||||||
onSave={handleSaveNode}
|
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));
|
export default withI18n()(withRouter(NodeModal));
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { func, number, shape, string } from 'prop-types';
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
|
|
||||||
function NodeNextButton({
|
function NodeNextButton({
|
||||||
i18n,
|
|
||||||
activeStep,
|
activeStep,
|
||||||
|
buttonText,
|
||||||
|
onClick,
|
||||||
onNext,
|
onNext,
|
||||||
triggerNext,
|
triggerNext,
|
||||||
onClick,
|
|
||||||
buttonText,
|
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!triggerNext) {
|
if (!triggerNext) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onNext();
|
onNext();
|
||||||
}, [triggerNext]);
|
}, [onNext, triggerNext]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<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 { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func, shape } from 'prop-types';
|
||||||
import { InventorySourcesAPI } from '@api';
|
import { InventorySourcesAPI } from '@api';
|
||||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
import PaginatedDataList from '@components/PaginatedDataList';
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
@@ -15,14 +16,14 @@ const QS_CONFIG = getQSConfig('inventory_sources', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function InventorySourcesList({
|
function InventorySourcesList({
|
||||||
i18n,
|
|
||||||
history,
|
history,
|
||||||
|
i18n,
|
||||||
nodeResource,
|
nodeResource,
|
||||||
updateNodeResource,
|
updateNodeResource,
|
||||||
}) {
|
}) {
|
||||||
const [inventorySources, setInventorySources] = useState([]);
|
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [inventorySources, setInventorySources] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,9 +48,24 @@ function InventorySourcesList({
|
|||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={error}
|
contentError={error}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading}
|
||||||
items={inventorySources}
|
|
||||||
itemCount={count}
|
itemCount={count}
|
||||||
|
items={inventorySources}
|
||||||
|
onRowClick={row => updateNodeResource(row)}
|
||||||
qsConfig={QS_CONFIG}
|
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={[
|
toolbarColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
@@ -58,24 +74,17 @@ function InventorySourcesList({
|
|||||||
isSearchable: true,
|
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));
|
export default withI18n()(withRouter(InventorySourcesList));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func, shape } from 'prop-types';
|
||||||
import { JobTemplatesAPI } from '@api';
|
import { JobTemplatesAPI } from '@api';
|
||||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
import PaginatedDataList from '@components/PaginatedDataList';
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
@@ -15,10 +16,10 @@ const QS_CONFIG = getQSConfig('job_templates', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function JobTemplatesList({ i18n, history, nodeResource, updateNodeResource }) {
|
function JobTemplatesList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||||
const [jobTemplates, setJobTemplates] = useState([]);
|
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [jobTemplates, setJobTemplates] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -44,9 +45,24 @@ function JobTemplatesList({ i18n, history, nodeResource, updateNodeResource }) {
|
|||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={error}
|
contentError={error}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading}
|
||||||
items={jobTemplates}
|
|
||||||
itemCount={count}
|
itemCount={count}
|
||||||
|
items={jobTemplates}
|
||||||
|
onRowClick={row => updateNodeResource(row)}
|
||||||
qsConfig={QS_CONFIG}
|
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={[
|
toolbarColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
@@ -55,24 +71,17 @@ function JobTemplatesList({ i18n, history, nodeResource, updateNodeResource }) {
|
|||||||
isSearchable: true,
|
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));
|
export default withI18n()(withRouter(JobTemplatesList));
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func, number, shape, string } from 'prop-types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Formik, Field } from 'formik';
|
import { Formik, Field } from 'formik';
|
||||||
import { Form, FormGroup, TextInput } from '@patternfly/react-core';
|
import { Form, FormGroup, TextInput } from '@patternfly/react-core';
|
||||||
import { Divider } from '@patternfly/react-core/dist/esm/experimental';
|
|
||||||
import FormRow from '@components/FormRow';
|
import FormRow from '@components/FormRow';
|
||||||
import AnsibleSelect from '@components/AnsibleSelect';
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
import VerticalSeperator from '@components/VerticalSeparator';
|
import VerticalSeperator from '@components/VerticalSeparator';
|
||||||
|
|
||||||
import InventorySourcesList from './InventorySourcesList';
|
import InventorySourcesList from './InventorySourcesList';
|
||||||
import JobTemplatesList from './JobTemplatesList';
|
import JobTemplatesList from './JobTemplatesList';
|
||||||
import ProjectsList from './ProjectsList';
|
import ProjectsList from './ProjectsList';
|
||||||
import WorkflowJobTemplatesList from './WorkflowJobTemplatesList';
|
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)`
|
const TimeoutInput = styled(TextInput)`
|
||||||
width: 200px;
|
width: 200px;
|
||||||
:not(:first-of-type) {
|
:not(:first-of-type) {
|
||||||
@@ -26,16 +32,16 @@ const TimeoutLabel = styled.p`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function NodeTypeStep({
|
function NodeTypeStep({
|
||||||
i18n,
|
|
||||||
nodeType = 'job_template',
|
|
||||||
updateNodeType,
|
|
||||||
nodeResource,
|
|
||||||
updateNodeResource,
|
|
||||||
name,
|
|
||||||
updateName,
|
|
||||||
description,
|
description,
|
||||||
|
i18n,
|
||||||
|
name,
|
||||||
|
nodeResource,
|
||||||
|
nodeType,
|
||||||
|
timeout,
|
||||||
updateDescription,
|
updateDescription,
|
||||||
timeout = 0,
|
updateName,
|
||||||
|
updateNodeResource,
|
||||||
|
updateNodeType,
|
||||||
updateTimeout,
|
updateTimeout,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -132,21 +138,21 @@ function NodeTypeStep({
|
|||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId="approval-name"
|
fieldId="approval-name"
|
||||||
isRequired={true}
|
isRequired
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
label={i18n._(t`Name`)}
|
label={i18n._(t`Name`)}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
autoFocus
|
||||||
id="approval-name"
|
id="approval-name"
|
||||||
isRequired={true}
|
isRequired
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
type="text"
|
type="text"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(value, event) => {
|
onChange={(value, evt) => {
|
||||||
updateName(value);
|
updateName(value);
|
||||||
field.onChange(event);
|
field.onChange(evt);
|
||||||
}}
|
}}
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
@@ -165,9 +171,9 @@ function NodeTypeStep({
|
|||||||
id="approval-description"
|
id="approval-description"
|
||||||
type="text"
|
type="text"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={value => {
|
onChange={(value, evt) => {
|
||||||
updateDescription(value);
|
updateDescription(value);
|
||||||
field.onChange(event);
|
field.onChange(evt);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -190,7 +196,7 @@ function NodeTypeStep({
|
|||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={value => {
|
onChange={(value, evt) => {
|
||||||
if (!value || value === '') {
|
if (!value || value === '') {
|
||||||
value = 0;
|
value = 0;
|
||||||
}
|
}
|
||||||
@@ -198,7 +204,7 @@ function NodeTypeStep({
|
|||||||
Number(value) * 60 +
|
Number(value) * 60 +
|
||||||
Number(form.values.timeoutSeconds)
|
Number(form.values.timeoutSeconds)
|
||||||
);
|
);
|
||||||
field.onChange(event);
|
field.onChange(evt);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TimeoutLabel>min</TimeoutLabel>
|
<TimeoutLabel>min</TimeoutLabel>
|
||||||
@@ -215,7 +221,7 @@ function NodeTypeStep({
|
|||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={value => {
|
onChange={(value, evt) => {
|
||||||
if (!value || value === '') {
|
if (!value || value === '') {
|
||||||
value = 0;
|
value = 0;
|
||||||
}
|
}
|
||||||
@@ -223,7 +229,7 @@ function NodeTypeStep({
|
|||||||
Number(value) +
|
Number(value) +
|
||||||
Number(form.values.timeoutMinutes) * 60
|
Number(form.values.timeoutMinutes) * 60
|
||||||
);
|
);
|
||||||
field.onChange(event);
|
field.onChange(evt);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TimeoutLabel>sec</TimeoutLabel>
|
<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);
|
export default withI18n()(NodeTypeStep);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func, shape } from 'prop-types';
|
||||||
import { ProjectsAPI } from '@api';
|
import { ProjectsAPI } from '@api';
|
||||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
import PaginatedDataList from '@components/PaginatedDataList';
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
@@ -14,11 +15,11 @@ const QS_CONFIG = getQSConfig('projects', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
function ProjectsList({ i18n, history, nodeResource, updateNodeResource }) {
|
function ProjectsList({ history, i18n, nodeResource, updateNodeResource }) {
|
||||||
const [projects, setProjects] = useState([]);
|
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [projects, setProjects] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -42,9 +43,24 @@ function ProjectsList({ i18n, history, nodeResource, updateNodeResource }) {
|
|||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={error}
|
contentError={error}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading}
|
||||||
items={projects}
|
|
||||||
itemCount={count}
|
itemCount={count}
|
||||||
|
items={projects}
|
||||||
|
onRowClick={row => updateNodeResource(row)}
|
||||||
qsConfig={QS_CONFIG}
|
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={[
|
toolbarColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
@@ -53,24 +69,17 @@ function ProjectsList({ i18n, history, nodeResource, updateNodeResource }) {
|
|||||||
isSearchable: true,
|
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));
|
export default withI18n()(withRouter(ProjectsList));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func, shape } from 'prop-types';
|
||||||
import { WorkflowJobTemplatesAPI } from '@api';
|
import { WorkflowJobTemplatesAPI } from '@api';
|
||||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
import PaginatedDataList from '@components/PaginatedDataList';
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
@@ -15,15 +16,15 @@ const QS_CONFIG = getQSConfig('workflow_job_templates', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function WorkflowJobTemplatesList({
|
function WorkflowJobTemplatesList({
|
||||||
i18n,
|
|
||||||
history,
|
history,
|
||||||
|
i18n,
|
||||||
nodeResource,
|
nodeResource,
|
||||||
updateNodeResource,
|
updateNodeResource,
|
||||||
}) {
|
}) {
|
||||||
const [workflowJobTemplates, setWorkflowJobTemplates] = useState([]);
|
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [workflowJobTemplates, setWorkflowJobTemplates] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -49,9 +50,24 @@ function WorkflowJobTemplatesList({
|
|||||||
<PaginatedDataList
|
<PaginatedDataList
|
||||||
contentError={error}
|
contentError={error}
|
||||||
hasContentLoading={isLoading}
|
hasContentLoading={isLoading}
|
||||||
items={workflowJobTemplates}
|
|
||||||
itemCount={count}
|
itemCount={count}
|
||||||
|
items={workflowJobTemplates}
|
||||||
|
onRowClick={row => updateNodeResource(row)}
|
||||||
qsConfig={QS_CONFIG}
|
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={[
|
toolbarColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
name: i18n._(t`Name`),
|
||||||
@@ -60,24 +76,17 @@ function WorkflowJobTemplatesList({
|
|||||||
isSearchable: true,
|
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));
|
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 { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { func, string } from 'prop-types';
|
||||||
import { Title } from '@patternfly/react-core';
|
import { Title } from '@patternfly/react-core';
|
||||||
import { SelectableCard } from '@components/SelectableCard';
|
import SelectableCard from '@components/SelectableCard';
|
||||||
|
|
||||||
const Grid = styled.div`
|
const Grid = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 33% 33% 33%;
|
|
||||||
grid-gap: 20px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
grid-auto-rows: 100px;
|
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;
|
margin: 20px 0px;
|
||||||
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function RunStep({ i18n, linkType, updateLinkType }) {
|
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);
|
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 React from 'react';
|
||||||
import { Button, Modal } from '@patternfly/react-core';
|
import { Button, Modal } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/macro';
|
import { t, Trans } from '@lingui/macro';
|
||||||
import { t } from '@lingui/macro';
|
import { func } from 'prop-types';
|
||||||
|
|
||||||
function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) {
|
function UnsavedChangesModal({ i18n, onCancel, onSaveAndExit, onExit }) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
width={600}
|
width={600}
|
||||||
isOpen={true}
|
isOpen
|
||||||
title={i18n._(t`Warning: Unsaved Changes`)}
|
title={i18n._(t`Warning: Unsaved Changes`)}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
actions={[
|
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);
|
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 { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { shape } from 'prop-types';
|
||||||
import { BaseSizes, Title, TitleLevel } from '@patternfly/react-core';
|
import { BaseSizes, Title, TitleLevel } from '@patternfly/react-core';
|
||||||
import { layoutGraph } from '@util/workflow';
|
import { layoutGraph } from '@util/workflow';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import DeleteAllNodesModal from './Modals/DeleteAllNodesModal';
|
import {
|
||||||
import LinkModal from './Modals/LinkModal';
|
DeleteAllNodesModal,
|
||||||
import LinkDeleteModal from './Modals/LinkDeleteModal';
|
LinkModal,
|
||||||
import NodeModal from './Modals/NodeModal/NodeModal';
|
LinkDeleteModal,
|
||||||
import NodeDeleteModal from './Modals/NodeDeleteModal';
|
NodeDeleteModal,
|
||||||
|
NodeViewModal,
|
||||||
|
UnsavedChangesModal,
|
||||||
|
} from './Modals';
|
||||||
|
import { NodeModal } from './Modals/NodeModal';
|
||||||
import VisualizerGraph from './VisualizerGraph';
|
import VisualizerGraph from './VisualizerGraph';
|
||||||
import VisualizerStartScreen from './VisualizerStartScreen';
|
import VisualizerStartScreen from './VisualizerStartScreen';
|
||||||
import VisualizerToolbar from './VisualizerToolbar';
|
import VisualizerToolbar from './VisualizerToolbar';
|
||||||
import UnsavedChangesModal from './Modals/UnsavedChangesModal';
|
|
||||||
import NodeViewModal from './Modals/NodeViewModal/NodeViewModal';
|
|
||||||
import {
|
import {
|
||||||
WorkflowApprovalTemplatesAPI,
|
WorkflowApprovalTemplatesAPI,
|
||||||
WorkflowJobTemplatesAPI,
|
|
||||||
WorkflowJobTemplateNodesAPI,
|
WorkflowJobTemplateNodesAPI,
|
||||||
|
WorkflowJobTemplatesAPI,
|
||||||
} from '@api';
|
} from '@api';
|
||||||
|
|
||||||
const CenteredContent = styled.div`
|
const CenteredContent = styled.div`
|
||||||
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -42,46 +45,42 @@ const fetchWorkflowNodes = async (
|
|||||||
pageNo = 1,
|
pageNo = 1,
|
||||||
workflowNodes = []
|
workflowNodes = []
|
||||||
) => {
|
) => {
|
||||||
try {
|
const { data } = await WorkflowJobTemplatesAPI.readNodes(templateId, {
|
||||||
const { data } = await WorkflowJobTemplatesAPI.readNodes(templateId, {
|
page_size: 200,
|
||||||
page_size: 200,
|
page: pageNo,
|
||||||
page: pageNo,
|
});
|
||||||
});
|
if (data.next) {
|
||||||
if (data.next) {
|
return fetchWorkflowNodes(
|
||||||
return await fetchWorkflowNodes(
|
templateId,
|
||||||
templateId,
|
pageNo + 1,
|
||||||
pageNo + 1,
|
workflowNodes.concat(data.results)
|
||||||
workflowNodes.concat(data.results)
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
return workflowNodes.concat(data.results);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
return workflowNodes.concat(data.results);
|
||||||
};
|
};
|
||||||
|
|
||||||
function Visualizer({ history, template, i18n }) {
|
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 [addLinkSourceNode, setAddLinkSourceNode] = useState(null);
|
||||||
const [addLinkTargetNode, setAddLinkTargetNode] = useState(null);
|
const [addLinkTargetNode, setAddLinkTargetNode] = useState(null);
|
||||||
const [addNodeSource, setAddNodeSource] = useState(null);
|
const [addNodeSource, setAddNodeSource] = useState(null);
|
||||||
const [addNodeTarget, setAddNodeTarget] = 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 [nextNodeId, setNextNodeId] = useState(0);
|
||||||
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
const [nodePositions, setNodePositions] = useState(null);
|
||||||
const [showUnsavedChangesModal, setShowUnsavedChangesModal] = useState(false);
|
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 [showDeleteAllNodesModal, setShowDeleteAllNodesModal] = useState(false);
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
const [showTools, setShowTools] = useState(false);
|
const [showTools, setShowTools] = useState(false);
|
||||||
|
const [showUnsavedChangesModal, setShowUnsavedChangesModal] = useState(false);
|
||||||
|
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
||||||
|
|
||||||
const startAddNode = (sourceNodeId, targetNodeId = null) => {
|
const startAddNode = (sourceNodeId, targetNodeId = null) => {
|
||||||
setAddNodeSource(sourceNodeId);
|
setAddNodeSource(sourceNodeId);
|
||||||
@@ -100,13 +99,13 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
// Ensures that root nodes appear to always run
|
// Ensures that root nodes appear to always run
|
||||||
// after "START"
|
// after "START"
|
||||||
if (addNodeSource === 1) {
|
if (addNodeSource === 1) {
|
||||||
newNode.edgeType = 'always';
|
newNode.linkType = 'always';
|
||||||
}
|
}
|
||||||
|
|
||||||
newLinks.push({
|
newLinks.push({
|
||||||
source: { id: addNodeSource },
|
source: { id: addNodeSource },
|
||||||
target: { id: nextNodeId },
|
target: { id: nextNodeId },
|
||||||
edgeType: newNode.edgeType,
|
linkType: newNode.linkType,
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
if (addNodeTarget) {
|
if (addNodeTarget) {
|
||||||
@@ -129,8 +128,8 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
setLinks(newLinks);
|
setLinks(newLinks);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startEditNode = nodeToEdit => {
|
const startEditNode = node => {
|
||||||
setNodeToEdit(nodeToEdit);
|
setNodeToEdit(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishEditingNode = editedNode => {
|
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 || link.target.id === nodeId) {
|
||||||
if (link.source.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) {
|
} else if (link.target.id === nodeId) {
|
||||||
parents.push(link.source.id);
|
parents.push(link.source.id);
|
||||||
}
|
}
|
||||||
@@ -193,7 +192,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
newLinks.push({
|
newLinks.push({
|
||||||
source: { id: parentId },
|
source: { id: parentId },
|
||||||
target: { id: child.id },
|
target: { id: child.id },
|
||||||
edgeType: 'always',
|
linkType: 'always',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -201,7 +200,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
newLinks.push({
|
newLinks.push({
|
||||||
source: { id: parentId },
|
source: { id: parentId },
|
||||||
target: { id: child.id },
|
target: { id: child.id },
|
||||||
edgeType: child.edgeType,
|
linkType: child.linkType,
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -217,14 +216,14 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
setLinks(newLinks);
|
setLinks(newLinks);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateLink = edgeType => {
|
const updateLink = linkType => {
|
||||||
const newLinks = [...links];
|
const newLinks = [...links];
|
||||||
newLinks.forEach(link => {
|
newLinks.forEach(link => {
|
||||||
if (
|
if (
|
||||||
link.source.id === linkToEdit.source.id &&
|
link.source.id === linkToEdit.source.id &&
|
||||||
link.target.id === linkToEdit.target.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 => {
|
const startDeleteLink = link => {
|
||||||
let parentMap = {};
|
const parentMap = {};
|
||||||
links.forEach(link => {
|
links.forEach(existingLink => {
|
||||||
if (!parentMap[link.target.id]) {
|
if (!parentMap[existingLink.target.id]) {
|
||||||
parentMap[link.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;
|
link.isConvergenceLink = parentMap[link.target.id].length > 1;
|
||||||
@@ -272,7 +271,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
target: {
|
target: {
|
||||||
id: linkToDelete.target.id,
|
id: linkToDelete.target.id,
|
||||||
},
|
},
|
||||||
edgeType: 'always',
|
linkType: 'always',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -286,8 +285,8 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
|
|
||||||
const selectSourceNodeForLinking = sourceNode => {
|
const selectSourceNodeForLinking = sourceNode => {
|
||||||
const newNodes = [...nodes];
|
const newNodes = [...nodes];
|
||||||
let parentMap = {};
|
const parentMap = {};
|
||||||
let invalidLinkTargetIds = [];
|
const invalidLinkTargetIds = [];
|
||||||
// Find and mark any ancestors as disabled to prevent cycles
|
// Find and mark any ancestors as disabled to prevent cycles
|
||||||
links.forEach(link => {
|
links.forEach(link => {
|
||||||
// id=1 is our artificial root node so we don't care about that
|
// 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]) {
|
if (parentMap[id]) {
|
||||||
parentMap[id].forEach(parentId => {
|
parentMap[id].forEach(parentId => {
|
||||||
invalidLinkTargetIds.push(parentId);
|
invalidLinkTargetIds.push(parentId);
|
||||||
@@ -334,7 +333,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
setAddLinkTargetNode(targetNode);
|
setAddLinkTargetNode(targetNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addLink = edgeType => {
|
const addLink = linkType => {
|
||||||
const newLinks = [...links];
|
const newLinks = [...links];
|
||||||
const newNodes = [...nodes];
|
const newNodes = [...nodes];
|
||||||
|
|
||||||
@@ -345,7 +344,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
newLinks.push({
|
newLinks.push({
|
||||||
source: { id: addLinkSourceNode.id },
|
source: { id: addLinkSourceNode.id },
|
||||||
target: { id: addLinkTargetNode.id },
|
target: { id: addLinkTargetNode.id },
|
||||||
edgeType,
|
linkType,
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -513,7 +512,6 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: error handling?
|
|
||||||
await Promise.all(nodeRequests);
|
await Promise.all(nodeRequests);
|
||||||
await Promise.all(approvalTemplateRequests);
|
await Promise.all(approvalTemplateRequests);
|
||||||
|
|
||||||
@@ -529,8 +527,8 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
if (!linkMap[realLinkSourceId]) {
|
if (!linkMap[realLinkSourceId]) {
|
||||||
linkMap[realLinkSourceId] = {};
|
linkMap[realLinkSourceId] = {};
|
||||||
}
|
}
|
||||||
linkMap[realLinkSourceId][realLinkTargetId] = link.edgeType;
|
linkMap[realLinkSourceId][realLinkTargetId] = link.linkType;
|
||||||
switch (link.edgeType) {
|
switch (link.linkType) {
|
||||||
case 'success':
|
case 'success':
|
||||||
if (
|
if (
|
||||||
!originalLinkMap[link.source.id].success_nodes.includes(
|
!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 => {
|
node.success_nodes.forEach(successNodeId => {
|
||||||
if (
|
if (
|
||||||
!deletedNodeIds.includes(successNodeId) &&
|
!deletedNodeIds.includes(successNodeId) &&
|
||||||
@@ -609,13 +608,12 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
// TODO: error handling?
|
|
||||||
await Promise.all(disassociateRequests);
|
await Promise.all(disassociateRequests);
|
||||||
|
|
||||||
newLinks.forEach(link => {
|
newLinks.forEach(link => {
|
||||||
switch (link.edgeType) {
|
switch (link.linkType) {
|
||||||
case 'success':
|
case 'success':
|
||||||
associateRequests.push(
|
associateRequests.push(
|
||||||
WorkflowJobTemplateNodesAPI.associateSuccessNode(
|
WorkflowJobTemplateNodesAPI.associateSuccessNode(
|
||||||
@@ -644,7 +642,6 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: error handling?
|
|
||||||
await Promise.all(associateRequests);
|
await Promise.all(associateRequests);
|
||||||
|
|
||||||
// Some nodes (both new and edited) are going to need a followup request to
|
// Some nodes (both new and edited) are going to need a followup request to
|
||||||
@@ -655,11 +652,8 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildGraphArrays = workflowNodes => {
|
const buildGraphArrays = workflowNodes => {
|
||||||
const nonRootNodeIds = [];
|
|
||||||
const allNodeIds = [];
|
const allNodeIds = [];
|
||||||
const arrayOfLinksForChart = [];
|
const arrayOfLinksForChart = [];
|
||||||
const nodeIdToChartNodeIdMapping = {};
|
|
||||||
const chartNodeIdToIndexMapping = {};
|
|
||||||
const arrayOfNodesForChart = [
|
const arrayOfNodesForChart = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -669,6 +663,9 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
type: 'node',
|
type: 'node',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const chartNodeIdToIndexMapping = {};
|
||||||
|
const nodeIdToChartNodeIdMapping = {};
|
||||||
|
const nonRootNodeIds = [];
|
||||||
let nodeIdCounter = 2;
|
let nodeIdCounter = 2;
|
||||||
// Assign each node an ID - 1 is reserved for the start node. We need to
|
// 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
|
// 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({
|
arrayOfLinksForChart.push({
|
||||||
source: arrayOfNodesForChart[sourceIndex],
|
source: arrayOfNodesForChart[sourceIndex],
|
||||||
target: arrayOfNodesForChart[targetIndex],
|
target: arrayOfNodesForChart[targetIndex],
|
||||||
edgeType: 'success',
|
linkType: 'success',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
nonRootNodeIds.push(nodeId);
|
nonRootNodeIds.push(nodeId);
|
||||||
@@ -715,7 +712,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
arrayOfLinksForChart.push({
|
arrayOfLinksForChart.push({
|
||||||
source: arrayOfNodesForChart[sourceIndex],
|
source: arrayOfNodesForChart[sourceIndex],
|
||||||
target: arrayOfNodesForChart[targetIndex],
|
target: arrayOfNodesForChart[targetIndex],
|
||||||
edgeType: 'failure',
|
linkType: 'failure',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
nonRootNodeIds.push(nodeId);
|
nonRootNodeIds.push(nodeId);
|
||||||
@@ -726,7 +723,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
arrayOfLinksForChart.push({
|
arrayOfLinksForChart.push({
|
||||||
source: arrayOfNodesForChart[sourceIndex],
|
source: arrayOfNodesForChart[sourceIndex],
|
||||||
target: arrayOfNodesForChart[targetIndex],
|
target: arrayOfNodesForChart[targetIndex],
|
||||||
edgeType: 'always',
|
linkType: 'always',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
nonRootNodeIds.push(nodeId);
|
nonRootNodeIds.push(nodeId);
|
||||||
@@ -745,7 +742,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
arrayOfLinksForChart.push({
|
arrayOfLinksForChart.push({
|
||||||
source: arrayOfNodesForChart[0],
|
source: arrayOfNodesForChart[0],
|
||||||
target: arrayOfNodesForChart[targetIndex],
|
target: arrayOfNodesForChart[targetIndex],
|
||||||
edgeType: 'always',
|
linkType: 'always',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -803,33 +800,33 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<VisualizerToolbar
|
<VisualizerToolbar
|
||||||
|
keyShown={showKey}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
template={template}
|
|
||||||
onClose={handleVisualizerClose}
|
onClose={handleVisualizerClose}
|
||||||
onSave={handleVisualizerSave}
|
|
||||||
onDeleteAllClick={() => setShowDeleteAllNodesModal(true)}
|
onDeleteAllClick={() => setShowDeleteAllNodesModal(true)}
|
||||||
onKeyToggle={() => setShowKey(!showKey)}
|
onKeyToggle={() => setShowKey(!showKey)}
|
||||||
keyShown={showKey}
|
onSave={handleVisualizerSave}
|
||||||
onToolsToggle={() => setShowTools(!showTools)}
|
onToolsToggle={() => setShowTools(!showTools)}
|
||||||
|
template={template}
|
||||||
toolsShown={showTools}
|
toolsShown={showTools}
|
||||||
/>
|
/>
|
||||||
{links.length > 0 ? (
|
{links.length > 0 ? (
|
||||||
<VisualizerGraph
|
<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}
|
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}
|
showKey={showKey}
|
||||||
showTools={showTools}
|
showTools={showTools}
|
||||||
/>
|
/>
|
||||||
@@ -837,58 +834,58 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
<VisualizerStartScreen onStartClick={startAddNode} />
|
<VisualizerStartScreen onStartClick={startAddNode} />
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
<NodeDeleteModal
|
{nodeToDelete && (
|
||||||
nodeToDelete={nodeToDelete}
|
<NodeDeleteModal
|
||||||
onConfirm={deleteNode}
|
nodeToDelete={nodeToDelete}
|
||||||
onCancel={() => setNodeToDelete(null)}
|
onCancel={() => setNodeToDelete(null)}
|
||||||
/>
|
onConfirm={deleteNode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{linkToDelete && (
|
{linkToDelete && (
|
||||||
<LinkDeleteModal
|
<LinkDeleteModal
|
||||||
linkToDelete={linkToDelete}
|
linkToDelete={linkToDelete}
|
||||||
onConfirm={deleteLink}
|
|
||||||
onCancel={() => setLinkToDelete(null)}
|
onCancel={() => setLinkToDelete(null)}
|
||||||
|
onConfirm={deleteLink}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{linkToEdit && (
|
{linkToEdit && (
|
||||||
<LinkModal
|
<LinkModal
|
||||||
|
linkType={linkToEdit.linkType}
|
||||||
header={
|
header={
|
||||||
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
|
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
|
||||||
{/* todo: make title match mockups (display: flex) */}
|
|
||||||
{i18n._(t`Edit Link`)}
|
{i18n._(t`Edit Link`)}
|
||||||
</Title>
|
</Title>
|
||||||
}
|
}
|
||||||
onConfirm={updateLink}
|
|
||||||
onCancel={() => setLinkToEdit(null)}
|
onCancel={() => setLinkToEdit(null)}
|
||||||
edgeType={linkToEdit.edgeType}
|
onConfirm={updateLink}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{addLinkSourceNode && addLinkTargetNode && (
|
{addLinkSourceNode && addLinkTargetNode && (
|
||||||
<LinkModal
|
<LinkModal
|
||||||
header={
|
header={
|
||||||
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
|
<Title headingLevel={TitleLevel.h1} size={BaseSizes['2xl']}>
|
||||||
{/* todo: make title match mockups (display: flex) */}
|
|
||||||
{i18n._(t`Add Link`)}
|
{i18n._(t`Add Link`)}
|
||||||
</Title>
|
</Title>
|
||||||
}
|
}
|
||||||
onConfirm={addLink}
|
|
||||||
onCancel={cancelNodeLink}
|
onCancel={cancelNodeLink}
|
||||||
|
onConfirm={addLink}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{addNodeSource && (
|
{addNodeSource && (
|
||||||
<NodeModal
|
<NodeModal
|
||||||
askLinkType={addNodeSource !== 1}
|
askLinkType={addNodeSource !== 1}
|
||||||
title={i18n._(t`Add Node`)}
|
|
||||||
onClose={() => cancelNodeForm()}
|
onClose={() => cancelNodeForm()}
|
||||||
onSave={finishAddingNode}
|
onSave={finishAddingNode}
|
||||||
|
title={i18n._(t`Add Node`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{nodeToEdit && (
|
{nodeToEdit && (
|
||||||
<NodeModal
|
<NodeModal
|
||||||
askLinkType={false}
|
askLinkType={false}
|
||||||
node={nodeToEdit}
|
nodeToEdit={nodeToEdit}
|
||||||
title={i18n._(t`Edit Node`)}
|
|
||||||
onClose={() => cancelNodeForm()}
|
onClose={() => cancelNodeForm()}
|
||||||
onSave={finishEditingNode}
|
onSave={finishEditingNode}
|
||||||
|
title={i18n._(t`Edit Node`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showUnsavedChangesModal && (
|
{showUnsavedChangesModal && (
|
||||||
@@ -915,4 +912,8 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Visualizer.propTypes = {
|
||||||
|
template: shape().isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(withRouter(Visualizer));
|
export default withI18n()(withRouter(Visualizer));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { arrayOf, bool, func, shape } from 'prop-types';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import {
|
import {
|
||||||
calcZoomAndFit,
|
calcZoomAndFit,
|
||||||
@@ -10,15 +11,15 @@ import {
|
|||||||
} from '@util/workflow';
|
} from '@util/workflow';
|
||||||
import {
|
import {
|
||||||
WorkflowHelp,
|
WorkflowHelp,
|
||||||
|
WorkflowKey,
|
||||||
WorkflowLinkHelp,
|
WorkflowLinkHelp,
|
||||||
WorkflowNodeHelp,
|
WorkflowNodeHelp,
|
||||||
|
WorkflowTools,
|
||||||
} from '@components/Workflow';
|
} from '@components/Workflow';
|
||||||
import {
|
import {
|
||||||
VisualizerLink,
|
VisualizerLink,
|
||||||
VisualizerNode,
|
VisualizerNode,
|
||||||
VisualizerStartNode,
|
VisualizerStartNode,
|
||||||
VisualizerKey,
|
|
||||||
VisualizerTools,
|
|
||||||
} from '@screens/Template/WorkflowJobTemplateVisualizer';
|
} from '@screens/Template/WorkflowJobTemplateVisualizer';
|
||||||
|
|
||||||
const PotentialLink = styled.polyline`
|
const PotentialLink = styled.polyline`
|
||||||
@@ -26,34 +27,34 @@ const PotentialLink = styled.polyline`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const WorkflowSVG = styled.svg`
|
const WorkflowSVG = styled.svg`
|
||||||
|
background-color: #f6f6f6;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #f6f6f6;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerGraph({
|
function VisualizerGraph({
|
||||||
|
addLinkSourceNode,
|
||||||
|
addingLink,
|
||||||
|
i18n,
|
||||||
links,
|
links,
|
||||||
nodes,
|
|
||||||
readOnly,
|
|
||||||
nodePositions,
|
nodePositions,
|
||||||
onDeleteNodeClick,
|
nodes,
|
||||||
onAddNodeClick,
|
onAddNodeClick,
|
||||||
|
onCancelAddLinkClick,
|
||||||
|
onConfirmAddLinkClick,
|
||||||
|
onDeleteLinkClick,
|
||||||
|
onDeleteNodeClick,
|
||||||
onEditNodeClick,
|
onEditNodeClick,
|
||||||
onLinkEditClick,
|
onLinkEditClick,
|
||||||
onDeleteLinkClick,
|
|
||||||
onStartAddLinkClick,
|
onStartAddLinkClick,
|
||||||
onConfirmAddLinkClick,
|
|
||||||
onCancelAddLinkClick,
|
|
||||||
onViewNodeClick,
|
onViewNodeClick,
|
||||||
addingLink,
|
readOnly,
|
||||||
addLinkSourceNode,
|
|
||||||
showKey,
|
showKey,
|
||||||
showTools,
|
showTools,
|
||||||
i18n,
|
|
||||||
}) {
|
}) {
|
||||||
const [helpText, setHelpText] = useState(null);
|
const [helpText, setHelpText] = useState(null);
|
||||||
const [nodeHelp, setNodeHelp] = useState();
|
|
||||||
const [linkHelp, setLinkHelp] = useState();
|
const [linkHelp, setLinkHelp] = useState();
|
||||||
|
const [nodeHelp, setNodeHelp] = useState();
|
||||||
const [zoomPercentage, setZoomPercentage] = useState(100);
|
const [zoomPercentage, setZoomPercentage] = useState(100);
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const gRef = useRef(null);
|
const gRef = useRef(null);
|
||||||
@@ -115,9 +116,10 @@ function VisualizerGraph({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePan = direction => {
|
const handlePan = direction => {
|
||||||
let { x: xPos, y: yPos, k: currentScale } = d3.zoomTransform(
|
const transform = d3.zoomTransform(d3.select(svgRef.current).node());
|
||||||
d3.select(svgRef.current).node()
|
|
||||||
);
|
let { x: xPos, y: yPos } = transform;
|
||||||
|
const { k: currentScale } = transform;
|
||||||
|
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case 'up':
|
case 'up':
|
||||||
@@ -223,23 +225,23 @@ function VisualizerGraph({
|
|||||||
<WorkflowSVG id="workflow-svg" ref={svgRef} css="">
|
<WorkflowSVG id="workflow-svg" ref={svgRef} css="">
|
||||||
<defs>
|
<defs>
|
||||||
<marker
|
<marker
|
||||||
id="workflow-triangle"
|
|
||||||
className="WorkflowChart-noPointerEvents"
|
className="WorkflowChart-noPointerEvents"
|
||||||
viewBox="0 -5 10 10"
|
id="workflow-triangle"
|
||||||
refX="10"
|
markerHeight="6"
|
||||||
markerUnits="strokeWidth"
|
markerUnits="strokeWidth"
|
||||||
markerWidth="6"
|
markerWidth="6"
|
||||||
markerHeight="6"
|
|
||||||
orient="auto"
|
orient="auto"
|
||||||
|
refX="10"
|
||||||
|
viewBox="0 -5 10 10"
|
||||||
>
|
>
|
||||||
<path d="M0,-5L10,0L0,5" fill="#93969A" />
|
<path d="M0,-5L10,0L0,5" fill="#93969A" />
|
||||||
</marker>
|
</marker>
|
||||||
</defs>
|
</defs>
|
||||||
<rect
|
<rect
|
||||||
width="100%"
|
|
||||||
height="100%"
|
height="100%"
|
||||||
opacity="0"
|
|
||||||
id="workflow-backround"
|
id="workflow-backround"
|
||||||
|
opacity="0"
|
||||||
|
width="100%"
|
||||||
{...(addingLink && {
|
{...(addingLink && {
|
||||||
onMouseMove: e => drawPotentialLinkToCursor(e),
|
onMouseMove: e => drawPotentialLinkToCursor(e),
|
||||||
onMouseOver: () =>
|
onMouseOver: () =>
|
||||||
@@ -255,12 +257,12 @@ function VisualizerGraph({
|
|||||||
<g id="workflow-g" ref={gRef}>
|
<g id="workflow-g" ref={gRef}>
|
||||||
{nodePositions && [
|
{nodePositions && [
|
||||||
<VisualizerStartNode
|
<VisualizerStartNode
|
||||||
|
addingLink={addingLink}
|
||||||
key="start"
|
key="start"
|
||||||
nodePositions={nodePositions}
|
nodePositions={nodePositions}
|
||||||
|
onAddNodeClick={onAddNodeClick}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
updateHelpText={setHelpText}
|
updateHelpText={setHelpText}
|
||||||
addingLink={addingLink}
|
|
||||||
onAddNodeClick={onAddNodeClick}
|
|
||||||
/>,
|
/>,
|
||||||
links.map(link => {
|
links.map(link => {
|
||||||
if (
|
if (
|
||||||
@@ -269,16 +271,16 @@ function VisualizerGraph({
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<VisualizerLink
|
<VisualizerLink
|
||||||
|
addingLink={addingLink}
|
||||||
key={`link-${link.source.id}-${link.target.id}`}
|
key={`link-${link.source.id}-${link.target.id}`}
|
||||||
link={link}
|
link={link}
|
||||||
nodePositions={nodePositions}
|
nodePositions={nodePositions}
|
||||||
|
onAddNodeClick={onAddNodeClick}
|
||||||
|
onDeleteLinkClick={onDeleteLinkClick}
|
||||||
|
onLinkEditClick={onLinkEditClick}
|
||||||
|
readOnly={readOnly}
|
||||||
updateHelpText={setHelpText}
|
updateHelpText={setHelpText}
|
||||||
updateLinkHelp={setLinkHelp}
|
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) {
|
if (node.id > 1 && nodePositions[node.id] && !node.isDeleted) {
|
||||||
return (
|
return (
|
||||||
<VisualizerNode
|
<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}
|
addingLink={addingLink}
|
||||||
isAddLinkSourceNode={
|
isAddLinkSourceNode={
|
||||||
addLinkSourceNode && addLinkSourceNode.id === node.id
|
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 && {
|
{...(addingLink && {
|
||||||
onMouseOver: () => drawPotentialLinkToNode(node),
|
onMouseOver: () => drawPotentialLinkToNode(node),
|
||||||
})}
|
})}
|
||||||
@@ -316,28 +318,52 @@ function VisualizerGraph({
|
|||||||
{addingLink && (
|
{addingLink && (
|
||||||
<PotentialLink
|
<PotentialLink
|
||||||
id="workflow-potentialLink"
|
id="workflow-potentialLink"
|
||||||
|
markerEnd="url(#workflow-triangle)"
|
||||||
|
stroke="#93969A"
|
||||||
strokeDasharray="5,5"
|
strokeDasharray="5,5"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
stroke="#93969A"
|
|
||||||
markerEnd="url(#workflow-triangle)"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
</WorkflowSVG>
|
</WorkflowSVG>
|
||||||
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
<div css="position: absolute; top: 75px;right: 20px;display: flex;">
|
||||||
{showTools && (
|
{showTools && (
|
||||||
<VisualizerTools
|
<WorkflowTools
|
||||||
zoomPercentage={zoomPercentage}
|
|
||||||
onZoomChange={handleZoomChange}
|
|
||||||
onFitGraph={handleFitGraph}
|
onFitGraph={handleFitGraph}
|
||||||
onPan={handlePan}
|
onPan={handlePan}
|
||||||
onPanToMiddle={handlePanToMiddle}
|
onPanToMiddle={handlePanToMiddle}
|
||||||
|
onZoomChange={handleZoomChange}
|
||||||
|
zoomPercentage={zoomPercentage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showKey && <VisualizerKey />}
|
{showKey && <WorkflowKey />}
|
||||||
</div>
|
</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);
|
export default withI18n()(VisualizerGraph);
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { bool, func, shape } from 'prop-types';
|
||||||
import { PencilAltIcon, PlusIcon, TrashAltIcon } from '@patternfly/react-icons';
|
import { PencilAltIcon, PlusIcon, TrashAltIcon } from '@patternfly/react-icons';
|
||||||
import {
|
import {
|
||||||
generateLine,
|
generateLine,
|
||||||
getLinkOverlayPoints,
|
|
||||||
getLinePoints,
|
getLinePoints,
|
||||||
|
getLinkOverlayPoints,
|
||||||
} from '@util/workflow';
|
} from '@util/workflow';
|
||||||
import {
|
import {
|
||||||
WorkflowActionTooltip,
|
WorkflowActionTooltip,
|
||||||
@@ -18,16 +19,16 @@ const LinkG = styled.g`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerLink({
|
function VisualizerLink({
|
||||||
|
addingLink,
|
||||||
|
i18n,
|
||||||
link,
|
link,
|
||||||
nodePositions,
|
nodePositions,
|
||||||
|
onAddNodeClick,
|
||||||
|
onDeleteLinkClick,
|
||||||
|
onLinkEditClick,
|
||||||
readOnly,
|
readOnly,
|
||||||
updateHelpText,
|
updateHelpText,
|
||||||
updateLinkHelp,
|
updateLinkHelp,
|
||||||
i18n,
|
|
||||||
onLinkEditClick,
|
|
||||||
onDeleteLinkClick,
|
|
||||||
addingLink,
|
|
||||||
onAddNodeClick,
|
|
||||||
}) {
|
}) {
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
const [pathD, setPathD] = useState();
|
const [pathD, setPathD] = useState();
|
||||||
@@ -61,18 +62,18 @@ function VisualizerLink({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="link-edit"
|
id="link-edit"
|
||||||
key="edit"
|
key="edit"
|
||||||
|
onClick={() => onLinkEditClick(link)}
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`Edit this link`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`Edit this link`))}
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
onClick={() => onLinkEditClick(link)}
|
|
||||||
>
|
>
|
||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="link-delete"
|
id="link-delete"
|
||||||
key="delete"
|
key="delete"
|
||||||
|
onClick={() => onDeleteLinkClick(link)}
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`Delete this link`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`Delete this link`))}
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
onClick={() => onDeleteLinkClick(link)}
|
|
||||||
>
|
>
|
||||||
<TrashAltIcon />
|
<TrashAltIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
@@ -95,16 +96,16 @@ function VisualizerLink({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (link.edgeType === 'failure') {
|
if (link.linkType === 'failure') {
|
||||||
setPathStroke('#d9534f');
|
setPathStroke('#d9534f');
|
||||||
}
|
}
|
||||||
if (link.edgeType === 'success') {
|
if (link.linkType === 'success') {
|
||||||
setPathStroke('#5cb85c');
|
setPathStroke('#5cb85c');
|
||||||
}
|
}
|
||||||
if (link.edgeType === 'always') {
|
if (link.linkType === 'always') {
|
||||||
setPathStroke('#337ab7');
|
setPathStroke('#337ab7');
|
||||||
}
|
}
|
||||||
}, [link.edgeType]);
|
}, [link.linkType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const linePoints = getLinePoints(link, nodePositions);
|
const linePoints = getLinePoints(link, nodePositions);
|
||||||
@@ -117,13 +118,13 @@ function VisualizerLink({
|
|||||||
<LinkG
|
<LinkG
|
||||||
className="WorkflowGraph-link"
|
className="WorkflowGraph-link"
|
||||||
id={`link-${link.source.id}-${link.target.id}`}
|
id={`link-${link.source.id}-${link.target.id}`}
|
||||||
|
ignorePointerEvents={addingLink}
|
||||||
onMouseEnter={handleLinkMouseEnter}
|
onMouseEnter={handleLinkMouseEnter}
|
||||||
onMouseLeave={handleLinkMouseLeave}
|
onMouseLeave={handleLinkMouseLeave}
|
||||||
ignorePointerEvents={addingLink}
|
|
||||||
>
|
>
|
||||||
<polygon
|
<polygon
|
||||||
id={`link-${link.source.id}-${link.target.id}-overlay`}
|
|
||||||
fill="#E1E1E1"
|
fill="#E1E1E1"
|
||||||
|
id={`link-${link.source.id}-${link.target.id}-overlay`}
|
||||||
opacity={hovering ? '1' : '0'}
|
opacity={hovering ? '1' : '0'}
|
||||||
points={getLinkOverlayPoints(link, nodePositions)}
|
points={getLinkOverlayPoints(link, nodePositions)}
|
||||||
/>
|
/>
|
||||||
@@ -134,20 +135,32 @@ function VisualizerLink({
|
|||||||
strokeWidth="2px"
|
strokeWidth="2px"
|
||||||
/>
|
/>
|
||||||
<polygon
|
<polygon
|
||||||
opacity="0"
|
|
||||||
points={getLinkOverlayPoints(link, nodePositions)}
|
|
||||||
onMouseEnter={() => updateLinkHelp(link)}
|
onMouseEnter={() => updateLinkHelp(link)}
|
||||||
onMouseLeave={() => updateLinkHelp(null)}
|
onMouseLeave={() => updateLinkHelp(null)}
|
||||||
|
opacity="0"
|
||||||
|
points={getLinkOverlayPoints(link, nodePositions)}
|
||||||
/>
|
/>
|
||||||
{!readOnly && hovering && (
|
{!readOnly && hovering && (
|
||||||
<WorkflowActionTooltip
|
<WorkflowActionTooltip
|
||||||
|
actions={tooltipActions}
|
||||||
pointX={tooltipX}
|
pointX={tooltipX}
|
||||||
pointY={tooltipY}
|
pointY={tooltipY}
|
||||||
actions={tooltipActions}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</LinkG>
|
</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);
|
export default withI18n()(VisualizerLink);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { bool, func, shape } from 'prop-types';
|
||||||
import {
|
import {
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
@@ -30,28 +31,28 @@ const NodeContents = styled.foreignObject`
|
|||||||
|
|
||||||
const NodeDefaultLabel = styled.p`
|
const NodeDefaultLabel = styled.p`
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerNode({
|
function VisualizerNode({
|
||||||
|
addingLink,
|
||||||
|
i18n,
|
||||||
|
isAddLinkSourceNode,
|
||||||
node,
|
node,
|
||||||
nodePositions,
|
nodePositions,
|
||||||
|
onAddNodeClick,
|
||||||
|
onConfirmAddLinkClick,
|
||||||
|
onDeleteNodeClick,
|
||||||
|
onEditNodeClick,
|
||||||
|
onMouseOver,
|
||||||
|
onStartAddLinkClick,
|
||||||
|
onViewNodeClick,
|
||||||
|
readOnly,
|
||||||
updateHelpText,
|
updateHelpText,
|
||||||
updateNodeHelp,
|
updateNodeHelp,
|
||||||
readOnly,
|
|
||||||
i18n,
|
|
||||||
onDeleteNodeClick,
|
|
||||||
onStartAddLinkClick,
|
|
||||||
onConfirmAddLinkClick,
|
|
||||||
addingLink,
|
|
||||||
onMouseOver,
|
|
||||||
isAddLinkSourceNode,
|
|
||||||
onAddNodeClick,
|
|
||||||
onEditNodeClick,
|
|
||||||
onViewNodeClick,
|
|
||||||
}) {
|
}) {
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
|
|
||||||
@@ -88,13 +89,13 @@ function VisualizerNode({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="node-details"
|
id="node-details"
|
||||||
key="details"
|
key="details"
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onViewNodeClick(node);
|
onViewNodeClick(node);
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
||||||
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
</WorkflowActionTooltipItem>
|
</WorkflowActionTooltipItem>
|
||||||
@@ -106,13 +107,13 @@ function VisualizerNode({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="node-add"
|
id="node-add"
|
||||||
key="add"
|
key="add"
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`Add a new node`))}
|
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onAddNodeClick(node.id);
|
onAddNodeClick(node.id);
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => updateHelpText(i18n._(t`Add a new node`))}
|
||||||
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
@@ -120,41 +121,41 @@ function VisualizerNode({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="node-edit"
|
id="node-edit"
|
||||||
key="edit"
|
key="edit"
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`Edit this node`))}
|
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onEditNodeClick(node);
|
onEditNodeClick(node);
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => updateHelpText(i18n._(t`Edit this node`))}
|
||||||
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="node-link"
|
id="node-link"
|
||||||
key="link"
|
key="link"
|
||||||
onMouseEnter={() =>
|
|
||||||
updateHelpText(i18n._(t`Link to an available node`))
|
|
||||||
}
|
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onStartAddLinkClick(node);
|
onStartAddLinkClick(node);
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() =>
|
||||||
|
updateHelpText(i18n._(t`Link to an available node`))
|
||||||
|
}
|
||||||
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<LinkIcon />
|
<LinkIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="node-delete"
|
id="node-delete"
|
||||||
key="delete"
|
key="delete"
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`Delete this node`))}
|
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
onDeleteNodeClick(node);
|
onDeleteNodeClick(node);
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => updateHelpText(i18n._(t`Delete this node`))}
|
||||||
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<TrashAltIcon />
|
<TrashAltIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
@@ -163,15 +164,15 @@ function VisualizerNode({
|
|||||||
return (
|
return (
|
||||||
<NodeG
|
<NodeG
|
||||||
id={`node-${node.id}`}
|
id={`node-${node.id}`}
|
||||||
transform={`translate(${nodePositions[node.id].x},${nodePositions[node.id]
|
|
||||||
.y - nodePositions[1].y})`}
|
|
||||||
job={node.job}
|
job={node.job}
|
||||||
noPointerEvents={isAddLinkSourceNode}
|
noPointerEvents={isAddLinkSourceNode}
|
||||||
onMouseEnter={handleNodeMouseEnter}
|
onMouseEnter={handleNodeMouseEnter}
|
||||||
onMouseLeave={handleNodeMouseLeave}
|
onMouseLeave={handleNodeMouseLeave}
|
||||||
|
transform={`translate(${nodePositions[node.id].x},${nodePositions[node.id]
|
||||||
|
.y - nodePositions[1].y})`}
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
width={wfConstants.nodeW}
|
fill="#FFFFFF"
|
||||||
height={wfConstants.nodeH}
|
height={wfConstants.nodeH}
|
||||||
rx="2"
|
rx="2"
|
||||||
ry="2"
|
ry="2"
|
||||||
@@ -181,17 +182,17 @@ function VisualizerNode({
|
|||||||
: '#93969A'
|
: '#93969A'
|
||||||
}
|
}
|
||||||
strokeWidth="4px"
|
strokeWidth="4px"
|
||||||
fill="#FFFFFF"
|
width={wfConstants.nodeW}
|
||||||
/>
|
/>
|
||||||
<NodeContents
|
<NodeContents
|
||||||
height="60"
|
height="60"
|
||||||
width="180"
|
|
||||||
isInvalidLinkTarget={node.isInvalidLinkTarget}
|
isInvalidLinkTarget={node.isInvalidLinkTarget}
|
||||||
{...(!addingLink && {
|
{...(!addingLink && {
|
||||||
onMouseEnter: () => updateNodeHelp(node),
|
onMouseEnter: () => updateNodeHelp(node),
|
||||||
onMouseLeave: () => updateNodeHelp(null),
|
onMouseLeave: () => updateNodeHelp(null),
|
||||||
})}
|
})}
|
||||||
onClick={() => handleNodeClick()}
|
onClick={() => handleNodeClick()}
|
||||||
|
width="180"
|
||||||
>
|
>
|
||||||
<NodeDefaultLabel>
|
<NodeDefaultLabel>
|
||||||
{node.unifiedJobTemplate
|
{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);
|
export default withI18n()(VisualizerNode);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { bool, func, shape } from 'prop-types';
|
||||||
import { PlusIcon } from '@patternfly/react-icons';
|
import { PlusIcon } from '@patternfly/react-icons';
|
||||||
import { constants as wfConstants } from '@util/workflow';
|
import { constants as wfConstants } from '@util/workflow';
|
||||||
import {
|
import {
|
||||||
@@ -14,12 +15,12 @@ const StartG = styled.g`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function VisualizerStartNode({
|
function VisualizerStartNode({
|
||||||
updateHelpText,
|
|
||||||
nodePositions,
|
|
||||||
readOnly,
|
|
||||||
i18n,
|
|
||||||
addingLink,
|
addingLink,
|
||||||
|
i18n,
|
||||||
|
nodePositions,
|
||||||
onAddNodeClick,
|
onAddNodeClick,
|
||||||
|
readOnly,
|
||||||
|
updateHelpText,
|
||||||
}) {
|
}) {
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
|
|
||||||
@@ -32,18 +33,18 @@ function VisualizerStartNode({
|
|||||||
return (
|
return (
|
||||||
<StartG
|
<StartG
|
||||||
id="node-1"
|
id="node-1"
|
||||||
transform={`translate(${nodePositions[1].x},0)`}
|
ignorePointerEvents={addingLink}
|
||||||
onMouseEnter={handleNodeMouseEnter}
|
onMouseEnter={handleNodeMouseEnter}
|
||||||
onMouseLeave={() => setHovering(false)}
|
onMouseLeave={() => setHovering(false)}
|
||||||
ignorePointerEvents={addingLink}
|
transform={`translate(${nodePositions[1].x},0)`}
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
width={wfConstants.rootW}
|
fill="#0279BC"
|
||||||
height={wfConstants.rootH}
|
height={wfConstants.rootH}
|
||||||
y="10"
|
|
||||||
rx="2"
|
rx="2"
|
||||||
ry="2"
|
ry="2"
|
||||||
fill="#0279BC"
|
width={wfConstants.rootW}
|
||||||
|
y="10"
|
||||||
/>
|
/>
|
||||||
{/* TODO: We need to be able to handle translated text here */}
|
{/* TODO: We need to be able to handle translated text here */}
|
||||||
<text x="13" y="30" dy=".35em" fill="white">
|
<text x="13" y="30" dy=".35em" fill="white">
|
||||||
@@ -51,8 +52,6 @@ function VisualizerStartNode({
|
|||||||
</text>
|
</text>
|
||||||
{!readOnly && hovering && (
|
{!readOnly && hovering && (
|
||||||
<WorkflowActionTooltip
|
<WorkflowActionTooltip
|
||||||
pointX={wfConstants.rootW}
|
|
||||||
pointY={wfConstants.rootH / 2 + 10}
|
|
||||||
actions={[
|
actions={[
|
||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="node-add"
|
id="node-add"
|
||||||
@@ -68,10 +67,20 @@ function VisualizerStartNode({
|
|||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
]}
|
]}
|
||||||
|
pointX={wfConstants.rootW}
|
||||||
|
pointY={wfConstants.rootH / 2 + 10}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StartG>
|
</StartG>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VisualizerStartNode.propTypes = {
|
||||||
|
addingLink: bool.isRequired,
|
||||||
|
nodePositions: shape().isRequired,
|
||||||
|
onAddNodeClick: func.isRequired,
|
||||||
|
readOnly: bool.isRequired,
|
||||||
|
updateHelpText: func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(VisualizerStartNode);
|
export default withI18n()(VisualizerStartNode);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { func } from 'prop-types';
|
||||||
import { Button as PFButton } from '@patternfly/react-core';
|
import { Button as PFButton } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -14,30 +15,30 @@ const Button = styled(PFButton)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StartPanel = styled.div`
|
const StartPanel = styled.div`
|
||||||
padding: 60px 80px;
|
|
||||||
border: 1px solid #c7c7c7;
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
border: 1px solid #c7c7c7;
|
||||||
|
padding: 60px 80px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StartPanelWrapper = styled.div`
|
const StartPanelWrapper = styled.div`
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function StartScreen({ i18n, onStartClick }) {
|
function VisualizerStartScreen({ i18n, onStartClick }) {
|
||||||
return (
|
return (
|
||||||
<div css="flex: 1">
|
<div css="flex: 1">
|
||||||
<StartPanelWrapper>
|
<StartPanelWrapper>
|
||||||
<StartPanel>
|
<StartPanel>
|
||||||
<p>{i18n._(t`Please click the Start button to begin.`)}</p>
|
<p>{i18n._(t`Please click the Start button to begin.`)}</p>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
|
||||||
aria-label={i18n._(t`Start`)}
|
aria-label={i18n._(t`Start`)}
|
||||||
onClick={() => onStartClick(1)}
|
onClick={() => onStartClick(1)}
|
||||||
|
variant="primary"
|
||||||
>
|
>
|
||||||
{i18n._(t`Start`)}
|
{i18n._(t`Start`)}
|
||||||
</Button>
|
</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 React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import { arrayOf, bool, func, shape } from 'prop-types';
|
||||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
BookIcon,
|
BookIcon,
|
||||||
CompassIcon,
|
CompassIcon,
|
||||||
DownloadIcon,
|
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
TimesIcon,
|
TimesIcon,
|
||||||
TrashAltIcon,
|
TrashAltIcon,
|
||||||
@@ -36,68 +36,50 @@ const ActionButton = styled(Button)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Toolbar({
|
function VisualizerToolbar({
|
||||||
i18n,
|
i18n,
|
||||||
template,
|
keyShown,
|
||||||
|
nodes,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
|
||||||
nodes = [],
|
|
||||||
onDeleteAllClick,
|
onDeleteAllClick,
|
||||||
onKeyToggle,
|
onKeyToggle,
|
||||||
keyShown,
|
onSave,
|
||||||
onToolsToggle,
|
onToolsToggle,
|
||||||
|
template,
|
||||||
toolsShown,
|
toolsShown,
|
||||||
}) {
|
}) {
|
||||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div css="align-items: center; border-bottom: 1px solid grey; display: flex; height: 56px; padding: 0px 20px;">
|
||||||
style={{
|
<div css="display: flex;">
|
||||||
borderBottom: '1px solid grey',
|
|
||||||
height: '56px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '0px 20px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<b>{i18n._(t`Workflow Visualizer`)}</b>
|
<b>{i18n._(t`Workflow Visualizer`)}</b>
|
||||||
<VerticalSeparator />
|
<VerticalSeparator />
|
||||||
<b>{template.name}</b>
|
<b>{template.name}</b>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div css="align-items: center; display: flex; flex: 1; justify-content: flex-end">
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flex: '1',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>{i18n._(t`Total Nodes`)}</div>
|
<div>{i18n._(t`Total Nodes`)}</div>
|
||||||
<Badge isRead>{totalNodes}</Badge>
|
<Badge isRead>{totalNodes}</Badge>
|
||||||
<VerticalSeparator />
|
<VerticalSeparator />
|
||||||
<Tooltip content={i18n._(t`Toggle Key`)} position="bottom">
|
<Tooltip content={i18n._(t`Toggle Key`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
variant="plain"
|
|
||||||
onClick={onKeyToggle}
|
|
||||||
isActive={keyShown}
|
isActive={keyShown}
|
||||||
|
onClick={onKeyToggle}
|
||||||
|
variant="plain"
|
||||||
>
|
>
|
||||||
<CompassIcon />
|
<CompassIcon />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
<Tooltip content={i18n._(t`Toggle Tools`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
variant="plain"
|
|
||||||
onClick={onToolsToggle}
|
|
||||||
isActive={toolsShown}
|
isActive={toolsShown}
|
||||||
|
onClick={onToolsToggle}
|
||||||
|
variant="plain"
|
||||||
>
|
>
|
||||||
<WrenchIcon />
|
<WrenchIcon />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ActionButton variant="plain" isDisabled>
|
|
||||||
<DownloadIcon />
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton variant="plain" isDisabled>
|
<ActionButton variant="plain" isDisabled>
|
||||||
<BookIcon />
|
<BookIcon />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@@ -106,10 +88,10 @@ function Toolbar({
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Tooltip content={i18n._(t`Delete All Nodes`)} position="bottom">
|
<Tooltip content={i18n._(t`Delete All Nodes`)} position="bottom">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
variant="plain"
|
|
||||||
isDisabled={totalNodes === 0}
|
|
||||||
aria-label={i18n._(t`Delete all nodes`)}
|
aria-label={i18n._(t`Delete all nodes`)}
|
||||||
|
isDisabled={totalNodes === 0}
|
||||||
onClick={onDeleteAllClick}
|
onClick={onDeleteAllClick}
|
||||||
|
variant="plain"
|
||||||
>
|
>
|
||||||
<TrashAltIcon />
|
<TrashAltIcon />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
@@ -120,9 +102,9 @@ function Toolbar({
|
|||||||
</Button>
|
</Button>
|
||||||
<VerticalSeparator />
|
<VerticalSeparator />
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
|
||||||
aria-label={i18n._(t`Close`)}
|
aria-label={i18n._(t`Close`)}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
variant="plain"
|
||||||
>
|
>
|
||||||
<TimesIcon />
|
<TimesIcon />
|
||||||
</Button>
|
</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 Visualizer } from './Visualizer';
|
||||||
export { default as VisualizerToolbar } from './VisualizerToolbar';
|
|
||||||
export { default as VisualizerGraph } from './VisualizerGraph';
|
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 VisualizerLink } from './VisualizerLink';
|
||||||
export { default as VisualizerNode } from './VisualizerNode';
|
export { default as VisualizerNode } from './VisualizerNode';
|
||||||
export { default as VisualizerKey } from './VisualizerKey';
|
export { default as VisualizerStartNode } from './VisualizerStartNode';
|
||||||
export { default as VisualizerTools } from './VisualizerTools';
|
export { default as VisualizerStartScreen } from './VisualizerStartScreen';
|
||||||
|
export { default as VisualizerToolbar } from './VisualizerToolbar';
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export function calcZoomAndFit(gRef, svgRef) {
|
|||||||
.node()
|
.node()
|
||||||
.getBoundingClientRect();
|
.getBoundingClientRect();
|
||||||
|
|
||||||
gBoundingClientRect.height = gBoundingClientRect.height / currentScale;
|
gBoundingClientRect.height /= currentScale;
|
||||||
gBoundingClientRect.width = gBoundingClientRect.width / currentScale;
|
gBoundingClientRect.width /= currentScale;
|
||||||
|
|
||||||
const gBBoxDimensions = d3
|
const gBBoxDimensions = d3
|
||||||
.select(gRef)
|
.select(gRef)
|
||||||
|
|||||||
Reference in New Issue
Block a user