mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 14:57:39 -02:30
Halfway implemented node details. Still need to handle cases where the user has edited the node and cases where the node is brand new.
This commit is contained in:
@@ -51,6 +51,10 @@ class WorkflowJobTemplateNodes extends Base {
|
|||||||
disassociate: true,
|
disassociate: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readCredentials(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WorkflowJobTemplateNodes;
|
export default WorkflowJobTemplateNodes;
|
||||||
|
|||||||
@@ -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 { Wizard } from '@patternfly/react-core';
|
|
||||||
import SelectResourceStep from './SelectResourceStep';
|
import SelectResourceStep from './SelectResourceStep';
|
||||||
import SelectRoleStep from './SelectRoleStep';
|
import SelectRoleStep from './SelectRoleStep';
|
||||||
import { SelectableCard } from '@components/SelectableCard';
|
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 =>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { string, number } from 'prop-types';
|
import { string, node, number } from 'prop-types';
|
||||||
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
|
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
|
||||||
import { DetailName, DetailValue } from '@components/DetailList';
|
import { DetailName, DetailValue } from '@components/DetailList';
|
||||||
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
|
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
|
||||||
@@ -21,7 +21,7 @@ function getValueAsMode(value, mode) {
|
|||||||
return mode === YAML_MODE ? jsonToYaml(value) : yamlToJson(value);
|
return mode === YAML_MODE ? jsonToYaml(value) : yamlToJson(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VariablesDetail({ value, label, rows }) {
|
function VariablesDetail({ value = '---', label, rows }) {
|
||||||
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
|
const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE);
|
||||||
const [currentValue, setCurrentValue] = useState(value || '---');
|
const [currentValue, setCurrentValue] = useState(value || '---');
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -90,7 +90,7 @@ function VariablesDetail({ value, label, rows }) {
|
|||||||
}
|
}
|
||||||
VariablesDetail.propTypes = {
|
VariablesDetail.propTypes = {
|
||||||
value: string.isRequired,
|
value: string.isRequired,
|
||||||
label: string.isRequired,
|
label: node.isRequired,
|
||||||
rows: number,
|
rows: number,
|
||||||
};
|
};
|
||||||
VariablesDetail.defaultProps = {
|
VariablesDetail.defaultProps = {
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { EmptyState, EmptyStateBody } from '@patternfly/react-core';
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
EmptyState as PFEmptyState,
|
||||||
|
EmptyStateBody,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const EmptyState = styled(PFEmptyState)`
|
||||||
|
--pf-c-empty-state--m-lg--MaxWidth: none;
|
||||||
|
`;
|
||||||
|
|
||||||
// TODO: Better loading state - skeleton lines / spinner, etc.
|
// TODO: Better loading state - skeleton lines / spinner, etc.
|
||||||
const ContentLoading = ({ className, i18n }) => (
|
const ContentLoading = ({ className, i18n }) => (
|
||||||
|
|||||||
@@ -25,8 +25,15 @@ const DetailValue = styled(({ fullWidth, ...props }) => (
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Detail = ({ label, value, fullWidth, className, dataCy }) => {
|
const Detail = ({
|
||||||
if (!value && typeof value !== 'number') {
|
label,
|
||||||
|
value,
|
||||||
|
fullWidth,
|
||||||
|
className,
|
||||||
|
dataCy,
|
||||||
|
alwaysVisible,
|
||||||
|
}) => {
|
||||||
|
if (!value && typeof value !== 'number' && !alwaysVisible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +65,12 @@ Detail.propTypes = {
|
|||||||
label: node.isRequired,
|
label: node.isRequired,
|
||||||
value: node,
|
value: node,
|
||||||
fullWidth: bool,
|
fullWidth: bool,
|
||||||
|
alwaysVisible: bool,
|
||||||
};
|
};
|
||||||
Detail.defaultProps = {
|
Detail.defaultProps = {
|
||||||
value: null,
|
value: null,
|
||||||
fullWidth: false,
|
fullWidth: false,
|
||||||
|
alwaysVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Detail;
|
export default Detail;
|
||||||
|
|||||||
@@ -10,14 +10,11 @@ import styled from 'styled-components';
|
|||||||
import VerticalSeparator from '../VerticalSeparator';
|
import VerticalSeparator from '../VerticalSeparator';
|
||||||
|
|
||||||
const Split = styled(PFSplit)`
|
const Split = styled(PFSplit)`
|
||||||
padding-top: 15px;
|
margin: 20px 0px;
|
||||||
padding-bottom: 5px;
|
|
||||||
border-bottom: #ebebeb var(--pf-global--BorderWidth--sm) solid;
|
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SplitLabelItem = styled(SplitItem)`
|
const SplitLabelItem = styled(SplitItem)`
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
word-break: initial;
|
word-break: initial;
|
||||||
`;
|
`;
|
||||||
|
|||||||
9
awx/ui_next/src/components/Wizard/Wizard.jsx
Normal file
9
awx/ui_next/src/components/Wizard/Wizard.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Wizard } from '@patternfly/react-core';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
Wizard.displayName = 'PFWizard';
|
||||||
|
export default styled(Wizard)`
|
||||||
|
.pf-c-data-toolbar__content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
10
awx/ui_next/src/components/Wizard/Wizard.test.jsx
Normal file
10
awx/ui_next/src/components/Wizard/Wizard.test.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import Wizard from './Wizard';
|
||||||
|
|
||||||
|
describe('Wizard', () => {
|
||||||
|
test('renders the expected content', () => {
|
||||||
|
const wrapper = mount(<Wizard />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
1
awx/ui_next/src/components/Wizard/index.js
Normal file
1
awx/ui_next/src/components/Wizard/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Wizard } from './Wizard';
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { Title } from '@patternfly/react-core';
|
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
|
||||||
import HorizontalSeparator from '@components/HorizontalSeparator';
|
|
||||||
|
|
||||||
function ApprovalPreviewStep({ i18n, name, description, timeout, linkType }) {
|
|
||||||
let linkTypeValue;
|
|
||||||
|
|
||||||
switch (linkType) {
|
|
||||||
case 'on_success':
|
|
||||||
linkTypeValue = i18n._(t`On Success`);
|
|
||||||
break;
|
|
||||||
case 'on_failure':
|
|
||||||
linkTypeValue = i18n._(t`On Failure`);
|
|
||||||
break;
|
|
||||||
case 'always':
|
|
||||||
linkTypeValue = i18n._(t`Always`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div>
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{i18n._(t`Approval Node`)}
|
|
||||||
</Title>
|
|
||||||
<HorizontalSeparator />
|
|
||||||
<DetailList>
|
|
||||||
<Detail label={i18n._(t`Name`)} value={name} />
|
|
||||||
<Detail label={i18n._(t`Description`)} value={description} />
|
|
||||||
<Detail label={i18n._(t`Timeout`)} value={timeoutValue} />
|
|
||||||
<Detail label={i18n._(t`Run`)} value={linkTypeValue} />
|
|
||||||
</DetailList>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n()(ApprovalPreviewStep);
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { Title } from '@patternfly/react-core';
|
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
|
||||||
import HorizontalSeparator from '@components/HorizontalSeparator';
|
|
||||||
|
|
||||||
function InventorySyncPreviewStep({ i18n, inventorySource, linkType }) {
|
|
||||||
let linkTypeValue;
|
|
||||||
|
|
||||||
switch (linkType) {
|
|
||||||
case 'success':
|
|
||||||
linkTypeValue = i18n._(t`On Success`);
|
|
||||||
break;
|
|
||||||
case 'failure':
|
|
||||||
linkTypeValue = i18n._(t`On Failure`);
|
|
||||||
break;
|
|
||||||
case 'always':
|
|
||||||
linkTypeValue = i18n._(t`Always`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{i18n._(t`Inventory Sync Node`)}
|
|
||||||
</Title>
|
|
||||||
<HorizontalSeparator />
|
|
||||||
<DetailList gutter="sm">
|
|
||||||
<Detail label={i18n._(t`Name`)} value={inventorySource.name} />
|
|
||||||
<Detail label={i18n._(t`Run`)} value={linkTypeValue} />
|
|
||||||
</DetailList>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n()(InventorySyncPreviewStep);
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { Title } from '@patternfly/react-core';
|
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
|
||||||
import HorizontalSeparator from '@components/HorizontalSeparator';
|
|
||||||
|
|
||||||
function JobTemplatePreviewStep({ i18n, jobTemplate, linkType }) {
|
|
||||||
let linkTypeValue;
|
|
||||||
|
|
||||||
switch (linkType) {
|
|
||||||
case 'success':
|
|
||||||
linkTypeValue = i18n._(t`On Success`);
|
|
||||||
break;
|
|
||||||
case 'failure':
|
|
||||||
linkTypeValue = i18n._(t`On Failure`);
|
|
||||||
break;
|
|
||||||
case 'always':
|
|
||||||
linkTypeValue = i18n._(t`Always`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{i18n._(t`Job Template Node`)}
|
|
||||||
</Title>
|
|
||||||
<HorizontalSeparator />
|
|
||||||
<DetailList gutter="sm">
|
|
||||||
<Detail label={i18n._(t`Name`)} value={jobTemplate.name} />
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Description`)}
|
|
||||||
value={jobTemplate.description}
|
|
||||||
/>
|
|
||||||
{/* <Detail label={i18n._(t`Job Type`)} value={job_type} />
|
|
||||||
|
|
||||||
{summary_fields.inventory ? (
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Inventory`)}
|
|
||||||
value={inventoryValue(
|
|
||||||
summary_fields.inventory.kind,
|
|
||||||
summary_fields.inventory.id
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
!ask_inventory_on_launch &&
|
|
||||||
renderMissingDataDetail(i18n._(t`Inventory`))
|
|
||||||
)}
|
|
||||||
{summary_fields.project ? (
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Project`)}
|
|
||||||
value={
|
|
||||||
<Link to={`/projects/${summary_fields.project.id}/details`}>
|
|
||||||
{summary_fields.project
|
|
||||||
? summary_fields.project.name
|
|
||||||
: i18n._(t`Deleted`)}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
renderMissingDataDetail(i18n._(t`Project`))
|
|
||||||
)}
|
|
||||||
<Detail label={i18n._(t`Playbook`)} value={playbook} />
|
|
||||||
<Detail label={i18n._(t`Forks`)} value={forks || '0'} />
|
|
||||||
<Detail label={i18n._(t`Limit`)} value={limit} />
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Verbosity`)}
|
|
||||||
value={verbosityDetails[0].details}
|
|
||||||
/>
|
|
||||||
<Detail label={i18n._(t`Timeout`)} value={timeout || '0'} />
|
|
||||||
{createdBy && (
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Created`)}
|
|
||||||
value={createdBy} // TODO: link to user in users
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{modifiedBy && (
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Last Modified`)}
|
|
||||||
value={modifiedBy} // TODO: link to user in users
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Show Changes`)}
|
|
||||||
value={diff_mode ? 'On' : 'Off'}
|
|
||||||
/>
|
|
||||||
<Detail label={i18n._(t` Job Slicing`)} value={job_slice_count} />
|
|
||||||
{host_config_key && (
|
|
||||||
<React.Fragment>
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Host Config Key`)}
|
|
||||||
value={host_config_key}
|
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Provisioning Callback URL`)}
|
|
||||||
value={generateCallBackUrl}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
{renderOptionsField && (
|
|
||||||
<Detail label={i18n._(t`Options`)} value={renderOptions} />
|
|
||||||
)}
|
|
||||||
{summary_fields.credentials &&
|
|
||||||
summary_fields.credentials.length > 0 && (
|
|
||||||
<Detail
|
|
||||||
fullWidth
|
|
||||||
label={i18n._(t`Credentials`)}
|
|
||||||
value={
|
|
||||||
<ChipGroup numChips={5}>
|
|
||||||
{summary_fields.credentials.map(c => (
|
|
||||||
<CredentialChip key={c.id} credential={c} isReadOnly />
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{job_tags && job_tags.length > 0 && (
|
|
||||||
<Detail
|
|
||||||
fullWidth
|
|
||||||
label={i18n._(t`Job tags`)}
|
|
||||||
value={
|
|
||||||
<ChipGroup numChips={5}>
|
|
||||||
{job_tags.split(',').map(jobTag => (
|
|
||||||
<Chip key={jobTag} isReadOnly>
|
|
||||||
{jobTag}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{skip_tags && skip_tags.length > 0 && (
|
|
||||||
<Detail
|
|
||||||
fullWidth
|
|
||||||
label={i18n._(t`Skip tags`)}
|
|
||||||
value={
|
|
||||||
<ChipGroup numChips={5}>
|
|
||||||
{skip_tags.split(',').map(skipTag => (
|
|
||||||
<Chip key={skipTag} isReadOnly>
|
|
||||||
{skipTag}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
<Detail label={i18n._(t`Run`)} value={linkTypeValue} />
|
|
||||||
</DetailList>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n()(JobTemplatePreviewStep);
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { Formik, Field } from 'formik';
|
|
||||||
import { Form, FormGroup, TextInput, Title } from '@patternfly/react-core';
|
|
||||||
import FormRow from '@components/FormRow';
|
|
||||||
import HorizontalSeparator from '@components/HorizontalSeparator';
|
|
||||||
|
|
||||||
const TimeoutInput = styled(TextInput)`
|
|
||||||
width: 200px;
|
|
||||||
:not(:first-of-type) {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TimeoutLabel = styled.p`
|
|
||||||
margin-left: 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function NodeApprovalStep({
|
|
||||||
i18n,
|
|
||||||
name,
|
|
||||||
updateName,
|
|
||||||
description,
|
|
||||||
updateDescription,
|
|
||||||
timeout = 0,
|
|
||||||
updateTimeout,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{i18n._(t`Approval Node`)}
|
|
||||||
</Title>
|
|
||||||
<HorizontalSeparator />
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
name: name || '',
|
|
||||||
description: description || '',
|
|
||||||
timeoutMinutes: Math.floor(timeout / 60),
|
|
||||||
timeoutSeconds: timeout - Math.floor(timeout / 60) * 60,
|
|
||||||
}}
|
|
||||||
render={() => (
|
|
||||||
<Form>
|
|
||||||
<FormRow>
|
|
||||||
<Field
|
|
||||||
name="name"
|
|
||||||
render={({ field, form }) => {
|
|
||||||
const isValid =
|
|
||||||
form &&
|
|
||||||
(!form.touched[field.name] || !form.errors[field.name]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormGroup
|
|
||||||
fieldId="approval-name"
|
|
||||||
isRequired={true}
|
|
||||||
isValid={isValid}
|
|
||||||
label={i18n._(t`Name`)}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
id="approval-name"
|
|
||||||
isRequired={true}
|
|
||||||
isValid={isValid}
|
|
||||||
type="text"
|
|
||||||
{...field}
|
|
||||||
onChange={(value, event) => {
|
|
||||||
updateName(value);
|
|
||||||
field.onChange(event);
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormRow>
|
|
||||||
<FormRow>
|
|
||||||
<Field
|
|
||||||
name="description"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormGroup
|
|
||||||
fieldId="approval-description"
|
|
||||||
label={i18n._(t`Description`)}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
id="approval-description"
|
|
||||||
type="text"
|
|
||||||
{...field}
|
|
||||||
onChange={value => {
|
|
||||||
updateDescription(value);
|
|
||||||
field.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormRow>
|
|
||||||
<FormRow>
|
|
||||||
<FormGroup label={i18n._(t`Timeout`)} fieldId="approval-timeout">
|
|
||||||
<div css="display: flex;align-items: center;">
|
|
||||||
<Field
|
|
||||||
name="timeoutMinutes"
|
|
||||||
render={({ field, form }) => (
|
|
||||||
<>
|
|
||||||
<TimeoutInput
|
|
||||||
id="approval-timeout-minutes"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
{...field}
|
|
||||||
onChange={value => {
|
|
||||||
if (!value || value === '') {
|
|
||||||
value = 0;
|
|
||||||
}
|
|
||||||
updateTimeout(
|
|
||||||
Number(value) * 60 +
|
|
||||||
Number(form.values.timeoutSeconds)
|
|
||||||
);
|
|
||||||
field.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TimeoutLabel>min</TimeoutLabel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name="timeoutSeconds"
|
|
||||||
render={({ field, form }) => (
|
|
||||||
<>
|
|
||||||
<TimeoutInput
|
|
||||||
id="approval-timeout-seconds"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
{...field}
|
|
||||||
onChange={value => {
|
|
||||||
if (!value || value === '') {
|
|
||||||
value = 0;
|
|
||||||
}
|
|
||||||
updateTimeout(
|
|
||||||
Number(value) +
|
|
||||||
Number(form.values.timeoutMinutes) * 60
|
|
||||||
);
|
|
||||||
field.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TimeoutLabel>sec</TimeoutLabel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormGroup>
|
|
||||||
</FormRow>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n()(NodeApprovalStep);
|
|
||||||
@@ -1,38 +1,26 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } 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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Wizard,
|
|
||||||
WizardContextConsumer,
|
WizardContextConsumer,
|
||||||
WizardFooter,
|
WizardFooter,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import NodeResourceStep from './NodeResourceStep';
|
import NodeTypeStep from './NodeTypeStep/NodeTypeStep';
|
||||||
import NodeTypeStep from './NodeTypeStep';
|
import RunStep from './RunStep';
|
||||||
import NodeNextButton from './NodeNextButton';
|
import NodeNextButton from './NodeNextButton';
|
||||||
import NodeApprovalStep from './NodeApprovalStep';
|
import { Wizard } from '@components/Wizard';
|
||||||
import ApprovalPreviewStep from './ApprovalPreviewStep';
|
|
||||||
import JobTemplatePreviewStep from './JobTemplatePreviewStep';
|
|
||||||
import InventorySyncPreviewStep from './InventorySyncPreviewStep';
|
|
||||||
import ProjectSyncPreviewStep from './ProjectSyncPreviewStep';
|
|
||||||
import WorkflowJobTemplatePreviewStep from './WorkflowJobTemplatePreviewStep';
|
|
||||||
|
|
||||||
import {
|
function NodeModal({
|
||||||
JobTemplatesAPI,
|
history,
|
||||||
ProjectsAPI,
|
i18n,
|
||||||
InventorySourcesAPI,
|
title,
|
||||||
WorkflowJobTemplatesAPI,
|
onClose,
|
||||||
} from '@api';
|
onSave,
|
||||||
|
node,
|
||||||
const readInventorySources = async queryParams =>
|
askLinkType,
|
||||||
InventorySourcesAPI.read(queryParams);
|
}) {
|
||||||
const readJobTemplates = async queryParams =>
|
|
||||||
JobTemplatesAPI.read(queryParams, { role_level: 'execute_role' });
|
|
||||||
const readProjects = async queryParams => ProjectsAPI.read(queryParams);
|
|
||||||
const readWorkflowJobTemplates = async queryParams =>
|
|
||||||
WorkflowJobTemplatesAPI.read(queryParams, { role_level: 'execute_role' });
|
|
||||||
|
|
||||||
function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
|
||||||
let defaultNodeType = 'job_template';
|
let defaultNodeType = 'job_template';
|
||||||
let defaultNodeResource = null;
|
let defaultNodeResource = null;
|
||||||
let defaultApprovalName = '';
|
let defaultApprovalName = '';
|
||||||
@@ -82,15 +70,6 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
|||||||
const [nodeType, setNodeType] = useState(defaultNodeType);
|
const [nodeType, setNodeType] = useState(defaultNodeType);
|
||||||
const [linkType, setLinkType] = useState('success');
|
const [linkType, setLinkType] = useState('success');
|
||||||
const [nodeResource, setNodeResource] = useState(defaultNodeResource);
|
const [nodeResource, setNodeResource] = useState(defaultNodeResource);
|
||||||
const [showApprovalStep, setShowApprovalStep] = useState(
|
|
||||||
defaultNodeType === 'approval'
|
|
||||||
);
|
|
||||||
const [showResourceStep, setShowResourceStep] = useState(
|
|
||||||
defaultNodeResource ? true : false
|
|
||||||
);
|
|
||||||
const [showPreviewStep, setShowPreviewStep] = useState(
|
|
||||||
defaultNodeType === 'approval' || defaultNodeResource ? true : false
|
|
||||||
);
|
|
||||||
const [triggerNext, setTriggerNext] = useState(0);
|
const [triggerNext, setTriggerNext] = useState(0);
|
||||||
const [approvalName, setApprovalName] = useState(defaultApprovalName);
|
const [approvalName, setApprovalName] = useState(defaultApprovalName);
|
||||||
const [approvalDescription, setApprovalDescription] = useState(
|
const [approvalDescription, setApprovalDescription] = useState(
|
||||||
@@ -100,7 +79,19 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
|||||||
defaultApprovalTimeout
|
defaultApprovalTimeout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const clearQueryParams = () => {
|
||||||
|
const parts = history.location.search.replace(/^\?/, '').split('&');
|
||||||
|
const otherParts = parts.filter(param =>
|
||||||
|
/^!(job_templates\.|projects\.|inventory_sources\.|workflow_job_templates\.)/.test(
|
||||||
|
param
|
||||||
|
)
|
||||||
|
);
|
||||||
|
history.push(`${history.location.pathname}?${otherParts.join('&')}`);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveNode = () => {
|
const handleSaveNode = () => {
|
||||||
|
clearQueryParams();
|
||||||
|
|
||||||
const resource =
|
const resource =
|
||||||
nodeType === 'approval'
|
nodeType === 'approval'
|
||||||
? {
|
? {
|
||||||
@@ -120,47 +111,13 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const resourceSearch = queryParams => {
|
const handleCancel = () => {
|
||||||
switch (nodeType) {
|
clearQueryParams();
|
||||||
case 'inventory_source_sync':
|
onClose();
|
||||||
return readInventorySources(queryParams);
|
|
||||||
case 'job_template':
|
|
||||||
return readJobTemplates(queryParams);
|
|
||||||
case 'project_sync':
|
|
||||||
return readProjects(queryParams);
|
|
||||||
case 'workflow_job_template':
|
|
||||||
return readWorkflowJobTemplates(queryParams);
|
|
||||||
default:
|
|
||||||
throw new Error(i18n._(t`Missing node type`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextClick = activeStep => {
|
|
||||||
if (activeStep.key === 'node_type') {
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
'inventory_source_sync',
|
|
||||||
'job_template',
|
|
||||||
'project_sync',
|
|
||||||
'workflow_job_template',
|
|
||||||
].includes(nodeType)
|
|
||||||
) {
|
|
||||||
setShowApprovalStep(false);
|
|
||||||
setShowResourceStep(true);
|
|
||||||
} else if (nodeType === 'approval') {
|
|
||||||
setShowResourceStep(false);
|
|
||||||
setShowApprovalStep(true);
|
|
||||||
}
|
|
||||||
setShowPreviewStep(true);
|
|
||||||
}
|
|
||||||
setTriggerNext(triggerNext + 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNodeTypeChange = newNodeType => {
|
const handleNodeTypeChange = newNodeType => {
|
||||||
setNodeType(newNodeType);
|
setNodeType(newNodeType);
|
||||||
setShowResourceStep(false);
|
|
||||||
setShowApprovalStep(false);
|
|
||||||
setShowPreviewStep(false);
|
|
||||||
setNodeResource(null);
|
setNodeResource(null);
|
||||||
setApprovalName('');
|
setApprovalName('');
|
||||||
setApprovalDescription('');
|
setApprovalDescription('');
|
||||||
@@ -168,101 +125,39 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
|
...(askLinkType
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: i18n._(t`Run Type`),
|
||||||
|
key: 'run_type',
|
||||||
|
component: (
|
||||||
|
<RunStep linkType={linkType} updateLinkType={setLinkType} />
|
||||||
|
),
|
||||||
|
enableNext: linkType !== null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
name: node ? i18n._(t`Node Type`) : i18n._(t`Run/Node Type`),
|
name: i18n._(t`Node Type`),
|
||||||
key: 'node_type',
|
key: 'node_resource',
|
||||||
|
enableNext:
|
||||||
|
(nodeType !== 'approval' && nodeResource !== null) ||
|
||||||
|
(nodeType === 'approval' && approvalName !== ''),
|
||||||
component: (
|
component: (
|
||||||
<NodeTypeStep
|
<NodeTypeStep
|
||||||
nodeType={nodeType}
|
nodeType={nodeType}
|
||||||
updateNodeType={handleNodeTypeChange}
|
updateNodeType={handleNodeTypeChange}
|
||||||
askLinkType={askLinkType}
|
nodeResource={nodeResource}
|
||||||
linkType={linkType}
|
updateNodeResource={setNodeResource}
|
||||||
updateLinkType={setLinkType}
|
name={approvalName}
|
||||||
|
updateName={setApprovalName}
|
||||||
|
description={approvalDescription}
|
||||||
|
updateDescription={setApprovalDescription}
|
||||||
|
timeout={approvalTimeout}
|
||||||
|
updateTimeout={setApprovalTimeout}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableNext: nodeType !== null,
|
|
||||||
},
|
},
|
||||||
...(showResourceStep
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: i18n._(t`Select Node Resource`),
|
|
||||||
key: 'node_resource',
|
|
||||||
enableNext: nodeResource !== null,
|
|
||||||
component: (
|
|
||||||
<NodeResourceStep
|
|
||||||
nodeType={nodeType}
|
|
||||||
search={resourceSearch}
|
|
||||||
nodeResource={nodeResource}
|
|
||||||
updateNodeResource={setNodeResource}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(showApprovalStep
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: i18n._(t`Configure Approval`),
|
|
||||||
key: 'approval',
|
|
||||||
component: (
|
|
||||||
<NodeApprovalStep
|
|
||||||
name={approvalName}
|
|
||||||
updateName={setApprovalName}
|
|
||||||
description={approvalDescription}
|
|
||||||
updateDescription={setApprovalDescription}
|
|
||||||
timeout={approvalTimeout}
|
|
||||||
updateTimeout={setApprovalTimeout}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
enableNext: approvalName !== '',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(showPreviewStep
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: i18n._(t`Preview`),
|
|
||||||
key: 'preview',
|
|
||||||
component: (
|
|
||||||
<>
|
|
||||||
{nodeType === 'approval' && (
|
|
||||||
<ApprovalPreviewStep
|
|
||||||
name={approvalName}
|
|
||||||
description={approvalDescription}
|
|
||||||
timeout={approvalTimeout}
|
|
||||||
linkType={linkType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{nodeType === 'job_template' && (
|
|
||||||
<JobTemplatePreviewStep
|
|
||||||
jobTemplate={nodeResource}
|
|
||||||
linkType={linkType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{nodeType === 'inventory_source_sync' && (
|
|
||||||
<InventorySyncPreviewStep
|
|
||||||
inventorySource={nodeResource}
|
|
||||||
linkType={linkType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{nodeType === 'project_sync' && (
|
|
||||||
<ProjectSyncPreviewStep
|
|
||||||
project={nodeResource}
|
|
||||||
linkType={linkType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{nodeType === 'workflow_job_template' && (
|
|
||||||
<WorkflowJobTemplatePreviewStep
|
|
||||||
workflowJobTemplate={nodeResource}
|
|
||||||
linkType={linkType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
enableNext: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
steps.forEach((step, n) => {
|
steps.forEach((step, n) => {
|
||||||
@@ -272,20 +167,25 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
|||||||
const CustomFooter = (
|
const CustomFooter = (
|
||||||
<WizardFooter>
|
<WizardFooter>
|
||||||
<WizardContextConsumer>
|
<WizardContextConsumer>
|
||||||
{({ activeStep, onNext, onBack, onClose }) => (
|
{({ activeStep, onNext, onBack }) => (
|
||||||
<>
|
<>
|
||||||
<NodeNextButton
|
<NodeNextButton
|
||||||
triggerNext={triggerNext}
|
triggerNext={triggerNext}
|
||||||
activeStep={activeStep}
|
activeStep={activeStep}
|
||||||
onNext={onNext}
|
onNext={onNext}
|
||||||
onClick={handleNextClick}
|
onClick={() => setTriggerNext(triggerNext + 1)}
|
||||||
|
buttonText={
|
||||||
|
activeStep.key === 'node_resource'
|
||||||
|
? i18n._(t`Save`)
|
||||||
|
: i18n._(t`Next`)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{activeStep && activeStep.id !== 1 && (
|
{activeStep && activeStep.id !== 1 && (
|
||||||
<Button variant="secondary" onClick={onBack}>
|
<Button variant="secondary" onClick={onBack}>
|
||||||
{i18n._(t`Back`)}
|
{i18n._(t`Back`)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button variant="link" onClick={onClose}>
|
<Button variant="link" onClick={handleCancel}>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -294,17 +194,19 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
|||||||
</WizardFooter>
|
</WizardFooter>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const wizardTitle = nodeResource ? `${title} | ${nodeResource.name}` : title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wizard
|
<Wizard
|
||||||
style={{ overflow: 'scroll' }}
|
style={{ overflow: 'scroll' }}
|
||||||
isOpen
|
isOpen
|
||||||
steps={steps}
|
steps={steps}
|
||||||
title={title}
|
title={wizardTitle}
|
||||||
onClose={onClose}
|
onClose={handleCancel}
|
||||||
onSave={handleSaveNode}
|
onSave={handleSaveNode}
|
||||||
footer={CustomFooter}
|
footer={CustomFooter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(NodeModal);
|
export default withI18n()(withRouter(NodeModal));
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
|
|
||||||
function NodeNextButton({ i18n, activeStep, onNext, triggerNext, onClick }) {
|
function NodeNextButton({
|
||||||
|
i18n,
|
||||||
|
activeStep,
|
||||||
|
onNext,
|
||||||
|
triggerNext,
|
||||||
|
onClick,
|
||||||
|
buttonText,
|
||||||
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!triggerNext) {
|
if (!triggerNext) {
|
||||||
return;
|
return;
|
||||||
@@ -18,7 +25,7 @@ function NodeNextButton({ i18n, activeStep, onNext, triggerNext, onClick }) {
|
|||||||
onClick={() => onClick(activeStep)}
|
onClick={() => onClick(activeStep)}
|
||||||
isDisabled={!activeStep.enableNext}
|
isDisabled={!activeStep.enableNext}
|
||||||
>
|
>
|
||||||
{activeStep.key === 'preview' ? i18n._(t`Save`) : i18n._(t`Next`)}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import React, { Fragment, useEffect, useState } from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { getQSConfig, parseQueryString } from '@util/qs';
|
|
||||||
import { Title } from '@patternfly/react-core';
|
|
||||||
import PaginatedDataList from '@components/PaginatedDataList';
|
|
||||||
import DataListToolbar from '@components/DataListToolbar';
|
|
||||||
import CheckboxListItem from '@components/CheckboxListItem';
|
|
||||||
import SelectedList from '@components/SelectedList';
|
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('node_resource', {
|
|
||||||
page: 1,
|
|
||||||
page_size: 5,
|
|
||||||
order_by: 'name',
|
|
||||||
});
|
|
||||||
|
|
||||||
function NodeTypeStep({
|
|
||||||
i18n,
|
|
||||||
search,
|
|
||||||
nodeType,
|
|
||||||
nodeResource,
|
|
||||||
updateNodeResource,
|
|
||||||
}) {
|
|
||||||
const [contentError, setContentError] = useState(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [rowCount, setRowCount] = useState(0);
|
|
||||||
const [rows, setRows] = useState([]);
|
|
||||||
|
|
||||||
let headerText = '';
|
|
||||||
|
|
||||||
switch (nodeType) {
|
|
||||||
case 'inventory_source_sync':
|
|
||||||
headerText = i18n._(t`Inventory Sources`);
|
|
||||||
break;
|
|
||||||
case 'job_template':
|
|
||||||
headerText = i18n._(t`Job Templates`);
|
|
||||||
break;
|
|
||||||
case 'project_sync':
|
|
||||||
headerText = i18n._(t`Projects`);
|
|
||||||
break;
|
|
||||||
case 'workflow_job_template':
|
|
||||||
headerText = i18n._(t`Workflow Job Templates`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchRows = queryString => {
|
|
||||||
const params = parseQueryString(QS_CONFIG, queryString);
|
|
||||||
return search(params);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
data: { count, results },
|
|
||||||
} = await fetchRows(location.node_resource);
|
|
||||||
|
|
||||||
setRows(results);
|
|
||||||
setRowCount(count);
|
|
||||||
} catch (error) {
|
|
||||||
setContentError(error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{headerText}
|
|
||||||
</Title>
|
|
||||||
<p>{i18n._(t`Select a resource to be executed from the list below.`)}</p>
|
|
||||||
{nodeResource && (
|
|
||||||
<SelectedList
|
|
||||||
displayKey="name"
|
|
||||||
label={i18n._(t`Selected`)}
|
|
||||||
onRemove={() => updateNodeResource(null)}
|
|
||||||
selected={[nodeResource]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PaginatedDataList
|
|
||||||
contentError={contentError}
|
|
||||||
hasContentLoading={isLoading}
|
|
||||||
items={rows}
|
|
||||||
itemCount={rowCount}
|
|
||||||
qsConfig={QS_CONFIG}
|
|
||||||
toolbarColumns={[
|
|
||||||
{
|
|
||||||
name: i18n._(t`Name`),
|
|
||||||
key: 'name',
|
|
||||||
isSortable: 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}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n()(NodeTypeStep);
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { Title } from '@patternfly/react-core';
|
|
||||||
import { SelectableCard } from '@components/SelectableCard';
|
|
||||||
|
|
||||||
const Grid = styled.div`
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 33% 33% 33%;
|
|
||||||
grid-gap: 20px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
||||||
grid-auto-rows: 100px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 20px 0px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function NodeTypeStep({
|
|
||||||
i18n,
|
|
||||||
nodeType,
|
|
||||||
updateNodeType,
|
|
||||||
linkType,
|
|
||||||
updateLinkType,
|
|
||||||
askLinkType,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{askLinkType && (
|
|
||||||
<>
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{i18n._(t`Run`)}
|
|
||||||
</Title>
|
|
||||||
<p>
|
|
||||||
{i18n._(
|
|
||||||
t`Specify the conditions under which this node should be executed`
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<Grid>
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={linkType === 'success'}
|
|
||||||
label={i18n._(t`On Success`)}
|
|
||||||
description={i18n._(
|
|
||||||
t`Execute when the parent node results in a successful state.`
|
|
||||||
)}
|
|
||||||
onClick={() => updateLinkType('success')}
|
|
||||||
/>
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={linkType === 'failure'}
|
|
||||||
label={i18n._(t`On Failure`)}
|
|
||||||
description={i18n._(
|
|
||||||
t`Execute when the parent node results in a failure state.`
|
|
||||||
)}
|
|
||||||
onClick={() => updateLinkType('failure')}
|
|
||||||
/>
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={linkType === 'always'}
|
|
||||||
label={i18n._(t`Always`)}
|
|
||||||
description={i18n._(
|
|
||||||
t`Execute regardless of the parent node's final state.`
|
|
||||||
)}
|
|
||||||
onClick={() => updateLinkType('always')}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{i18n._(t`Node Type`)}
|
|
||||||
</Title>
|
|
||||||
<Grid>
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={nodeType === 'job_template'}
|
|
||||||
label={i18n._(t`Job Template`)}
|
|
||||||
description={i18n._(t`Execute a job template.`)}
|
|
||||||
onClick={() => updateNodeType('job_template')}
|
|
||||||
/>
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={nodeType === 'workflow_job_template'}
|
|
||||||
label={i18n._(t`Workflow Job Template`)}
|
|
||||||
description={i18n._(t`Execute a workflow job template.`)}
|
|
||||||
onClick={() => updateNodeType('workflow_job_template')}
|
|
||||||
/>
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={nodeType === 'project_sync'}
|
|
||||||
label={i18n._(t`Project Sync`)}
|
|
||||||
description={i18n._(t`Execute a project sync.`)}
|
|
||||||
onClick={() => updateNodeType('project_sync')}
|
|
||||||
/>
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={nodeType === 'inventory_source_sync'}
|
|
||||||
label={i18n._(t`Inventory Source Sync`)}
|
|
||||||
description={i18n._(t`Execute an inventory source sync.`)}
|
|
||||||
onClick={() => updateNodeType('inventory_source_sync')}
|
|
||||||
/>
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={nodeType === 'approval'}
|
|
||||||
label={i18n._(t`Approval`)}
|
|
||||||
description={i18n._(t`Pause the workflow and wait for approval.`)}
|
|
||||||
onClick={() => updateNodeType('approval')}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n()(NodeTypeStep);
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { InventorySourcesAPI } from '@api';
|
||||||
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
|
import CheckboxListItem from '@components/CheckboxListItem';
|
||||||
|
|
||||||
|
const QS_CONFIG = getQSConfig('inventory_sources', {
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
function InventorySourcesList({
|
||||||
|
i18n,
|
||||||
|
history,
|
||||||
|
nodeResource,
|
||||||
|
updateNodeResource,
|
||||||
|
}) {
|
||||||
|
const [inventorySources, setInventorySources] = useState([]);
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setInventorySources([]);
|
||||||
|
setCount(0);
|
||||||
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
|
try {
|
||||||
|
const { data } = await InventorySourcesAPI.read(params);
|
||||||
|
setInventorySources(data.results);
|
||||||
|
setCount(data.count);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [history.location]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={error}
|
||||||
|
hasContentLoading={isLoading}
|
||||||
|
items={inventorySources}
|
||||||
|
itemCount={count}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
toolbarColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isSortable: 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(withRouter(InventorySourcesList));
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { JobTemplatesAPI } from '@api';
|
||||||
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
|
import CheckboxListItem from '@components/CheckboxListItem';
|
||||||
|
|
||||||
|
const QS_CONFIG = getQSConfig('job_templates', {
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
function JobTemplatesList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||||
|
const [jobTemplates, setJobTemplates] = useState([]);
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setJobTemplates([]);
|
||||||
|
setCount(0);
|
||||||
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
|
try {
|
||||||
|
const { data } = await JobTemplatesAPI.read(params, {
|
||||||
|
role_level: 'execute_role',
|
||||||
|
});
|
||||||
|
setJobTemplates(data.results);
|
||||||
|
setCount(data.count);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [history.location]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={error}
|
||||||
|
hasContentLoading={isLoading}
|
||||||
|
items={jobTemplates}
|
||||||
|
itemCount={count}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
toolbarColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isSortable: 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(withRouter(JobTemplatesList));
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Formik, Field } from 'formik';
|
||||||
|
import { Form, FormGroup, TextInput } from '@patternfly/react-core';
|
||||||
|
import { Divider } from '@patternfly/react-core/dist/esm/experimental';
|
||||||
|
import FormRow from '@components/FormRow';
|
||||||
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
|
import VerticalSeperator from '@components/VerticalSeparator';
|
||||||
|
|
||||||
|
import InventorySourcesList from './InventorySourcesList';
|
||||||
|
import JobTemplatesList from './JobTemplatesList';
|
||||||
|
import ProjectsList from './ProjectsList';
|
||||||
|
import WorkflowJobTemplatesList from './WorkflowJobTemplatesList';
|
||||||
|
|
||||||
|
const TimeoutInput = styled(TextInput)`
|
||||||
|
width: 200px;
|
||||||
|
:not(:first-of-type) {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TimeoutLabel = styled.p`
|
||||||
|
margin-left: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function NodeTypeStep({
|
||||||
|
i18n,
|
||||||
|
nodeType = 'job_template',
|
||||||
|
updateNodeType,
|
||||||
|
nodeResource,
|
||||||
|
updateNodeResource,
|
||||||
|
name,
|
||||||
|
updateName,
|
||||||
|
description,
|
||||||
|
updateDescription,
|
||||||
|
timeout = 0,
|
||||||
|
updateTimeout,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div css=" display: flex; align-items: center; margin-bottom: 20px;">
|
||||||
|
<b>{i18n._(t`Node Type`)}</b>
|
||||||
|
<VerticalSeperator />
|
||||||
|
<div>
|
||||||
|
<AnsibleSelect
|
||||||
|
id="nodeResource-select"
|
||||||
|
label={i18n._(t`Select a Node Type`)}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
key: 'approval',
|
||||||
|
value: 'approval',
|
||||||
|
label: i18n._(t`Approval`),
|
||||||
|
isDisabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'inventory_source_sync',
|
||||||
|
value: 'inventory_source_sync',
|
||||||
|
label: i18n._(t`Inventory Source Sync`),
|
||||||
|
isDisabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'job_template',
|
||||||
|
value: 'job_template',
|
||||||
|
label: i18n._(t`Job Template`),
|
||||||
|
isDisabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'project_sync',
|
||||||
|
value: 'project_sync',
|
||||||
|
label: i18n._(t`Project Sync`),
|
||||||
|
isDisabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'workflow_job_template',
|
||||||
|
value: 'workflow_job_template',
|
||||||
|
label: i18n._(t`Workflow Job Template`),
|
||||||
|
isDisabled: false,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={nodeType}
|
||||||
|
onChange={(e, val) => {
|
||||||
|
updateNodeType(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider component="div" />
|
||||||
|
{nodeType === 'job_template' && (
|
||||||
|
<JobTemplatesList
|
||||||
|
nodeResource={nodeResource}
|
||||||
|
updateNodeResource={updateNodeResource}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{nodeType === 'project_sync' && (
|
||||||
|
<ProjectsList
|
||||||
|
nodeResource={nodeResource}
|
||||||
|
updateNodeResource={updateNodeResource}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{nodeType === 'inventory_source_sync' && (
|
||||||
|
<InventorySourcesList
|
||||||
|
nodeResource={nodeResource}
|
||||||
|
updateNodeResource={updateNodeResource}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{nodeType === 'workflow_job_template' && (
|
||||||
|
<WorkflowJobTemplatesList
|
||||||
|
nodeResource={nodeResource}
|
||||||
|
updateNodeResource={updateNodeResource}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{nodeType === 'approval' && (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
name: name || '',
|
||||||
|
description: description || '',
|
||||||
|
timeoutMinutes: Math.floor(timeout / 60),
|
||||||
|
timeoutSeconds: timeout - Math.floor(timeout / 60) * 60,
|
||||||
|
}}
|
||||||
|
render={() => (
|
||||||
|
<Form css="margin-top: 20px;">
|
||||||
|
<FormRow>
|
||||||
|
<Field
|
||||||
|
name="name"
|
||||||
|
render={({ field, form }) => {
|
||||||
|
const isValid =
|
||||||
|
form &&
|
||||||
|
(!form.touched[field.name] || !form.errors[field.name]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId="approval-name"
|
||||||
|
isRequired={true}
|
||||||
|
isValid={isValid}
|
||||||
|
label={i18n._(t`Name`)}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="approval-name"
|
||||||
|
isRequired={true}
|
||||||
|
isValid={isValid}
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
onChange={(value, event) => {
|
||||||
|
updateName(value);
|
||||||
|
field.onChange(event);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<Field
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormGroup
|
||||||
|
fieldId="approval-description"
|
||||||
|
label={i18n._(t`Description`)}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="approval-description"
|
||||||
|
type="text"
|
||||||
|
{...field}
|
||||||
|
onChange={value => {
|
||||||
|
updateDescription(value);
|
||||||
|
field.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<FormGroup
|
||||||
|
label={i18n._(t`Timeout`)}
|
||||||
|
fieldId="approval-timeout"
|
||||||
|
>
|
||||||
|
<div css="display: flex;align-items: center;">
|
||||||
|
<Field
|
||||||
|
name="timeoutMinutes"
|
||||||
|
render={({ field, form }) => (
|
||||||
|
<>
|
||||||
|
<TimeoutInput
|
||||||
|
id="approval-timeout-minutes"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
{...field}
|
||||||
|
onChange={value => {
|
||||||
|
if (!value || value === '') {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
updateTimeout(
|
||||||
|
Number(value) * 60 +
|
||||||
|
Number(form.values.timeoutSeconds)
|
||||||
|
);
|
||||||
|
field.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TimeoutLabel>min</TimeoutLabel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name="timeoutSeconds"
|
||||||
|
render={({ field, form }) => (
|
||||||
|
<>
|
||||||
|
<TimeoutInput
|
||||||
|
id="approval-timeout-seconds"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
{...field}
|
||||||
|
onChange={value => {
|
||||||
|
if (!value || value === '') {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
updateTimeout(
|
||||||
|
Number(value) +
|
||||||
|
Number(form.values.timeoutMinutes) * 60
|
||||||
|
);
|
||||||
|
field.onChange(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TimeoutLabel>sec</TimeoutLabel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
</FormRow>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(NodeTypeStep);
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { ProjectsAPI } from '@api';
|
||||||
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
|
import CheckboxListItem from '@components/CheckboxListItem';
|
||||||
|
|
||||||
|
const QS_CONFIG = getQSConfig('projects', {
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
function ProjectsList({ i18n, history, nodeResource, updateNodeResource }) {
|
||||||
|
const [projects, setProjects] = useState([]);
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setProjects([]);
|
||||||
|
setCount(0);
|
||||||
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
|
try {
|
||||||
|
const { data } = await ProjectsAPI.read(params);
|
||||||
|
setProjects(data.results);
|
||||||
|
setCount(data.count);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [history.location]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={error}
|
||||||
|
hasContentLoading={isLoading}
|
||||||
|
items={projects}
|
||||||
|
itemCount={count}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
toolbarColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isSortable: 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(withRouter(ProjectsList));
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { WorkflowJobTemplatesAPI } from '@api';
|
||||||
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
|
import PaginatedDataList from '@components/PaginatedDataList';
|
||||||
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
|
import CheckboxListItem from '@components/CheckboxListItem';
|
||||||
|
|
||||||
|
const QS_CONFIG = getQSConfig('workflow_job_templates', {
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
function WorkflowJobTemplatesList({
|
||||||
|
i18n,
|
||||||
|
history,
|
||||||
|
nodeResource,
|
||||||
|
updateNodeResource,
|
||||||
|
}) {
|
||||||
|
const [workflowJobTemplates, setWorkflowJobTemplates] = useState([]);
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setWorkflowJobTemplates([]);
|
||||||
|
setCount(0);
|
||||||
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
|
try {
|
||||||
|
const { data } = await WorkflowJobTemplatesAPI.read(params, {
|
||||||
|
role_level: 'execute_role',
|
||||||
|
});
|
||||||
|
setWorkflowJobTemplates(data.results);
|
||||||
|
setCount(data.count);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [history.location]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={error}
|
||||||
|
hasContentLoading={isLoading}
|
||||||
|
items={workflowJobTemplates}
|
||||||
|
itemCount={count}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
toolbarColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isSortable: 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(withRouter(WorkflowJobTemplatesList));
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { Title } from '@patternfly/react-core';
|
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
|
||||||
import HorizontalSeparator from '@components/HorizontalSeparator';
|
|
||||||
|
|
||||||
function ProjectPreviewStep({ i18n, project, linkType }) {
|
|
||||||
let linkTypeValue;
|
|
||||||
|
|
||||||
switch (linkType) {
|
|
||||||
case 'success':
|
|
||||||
linkTypeValue = i18n._(t`On Success`);
|
|
||||||
break;
|
|
||||||
case 'failure':
|
|
||||||
linkTypeValue = i18n._(t`On Failure`);
|
|
||||||
break;
|
|
||||||
case 'always':
|
|
||||||
linkTypeValue = i18n._(t`Always`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{i18n._(t`Project Sync Node`)}
|
|
||||||
</Title>
|
|
||||||
<HorizontalSeparator />
|
|
||||||
<DetailList gutter="sm">
|
|
||||||
<Detail label={i18n._(t`Name`)} value={project.name} />
|
|
||||||
<Detail label={i18n._(t`Run`)} value={linkTypeValue} />
|
|
||||||
</DetailList>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n()(ProjectPreviewStep);
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Title } from '@patternfly/react-core';
|
||||||
|
import { SelectableCard } from '@components/SelectableCard';
|
||||||
|
|
||||||
|
const Grid = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 33% 33% 33%;
|
||||||
|
grid-gap: 20px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
grid-auto-rows: 100px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function RunStep({ i18n, linkType, updateLinkType }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title headingLevel="h1" size="xl">
|
||||||
|
{i18n._(t`Run`)}
|
||||||
|
</Title>
|
||||||
|
<p>
|
||||||
|
{i18n._(
|
||||||
|
t`Specify the conditions under which this node should be executed`
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<Grid>
|
||||||
|
<SelectableCard
|
||||||
|
isSelected={linkType === 'success'}
|
||||||
|
label={i18n._(t`On Success`)}
|
||||||
|
description={i18n._(
|
||||||
|
t`Execute when the parent node results in a successful state.`
|
||||||
|
)}
|
||||||
|
onClick={() => updateLinkType('success')}
|
||||||
|
/>
|
||||||
|
<SelectableCard
|
||||||
|
isSelected={linkType === 'failure'}
|
||||||
|
label={i18n._(t`On Failure`)}
|
||||||
|
description={i18n._(
|
||||||
|
t`Execute when the parent node results in a failure state.`
|
||||||
|
)}
|
||||||
|
onClick={() => updateLinkType('failure')}
|
||||||
|
/>
|
||||||
|
<SelectableCard
|
||||||
|
isSelected={linkType === 'always'}
|
||||||
|
label={i18n._(t`Always`)}
|
||||||
|
description={i18n._(
|
||||||
|
t`Execute regardless of the parent node's final state.`
|
||||||
|
)}
|
||||||
|
onClick={() => updateLinkType('always')}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(RunStep);
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { Title } from '@patternfly/react-core';
|
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
|
||||||
import HorizontalSeparator from '@components/HorizontalSeparator';
|
|
||||||
|
|
||||||
function WorkflowJobTemplatePreviewStep({
|
|
||||||
i18n,
|
|
||||||
workflowJobTemplate,
|
|
||||||
linkType,
|
|
||||||
}) {
|
|
||||||
let linkTypeValue;
|
|
||||||
|
|
||||||
switch (linkType) {
|
|
||||||
case 'success':
|
|
||||||
linkTypeValue = i18n._(t`On Success`);
|
|
||||||
break;
|
|
||||||
case 'failure':
|
|
||||||
linkTypeValue = i18n._(t`On Failure`);
|
|
||||||
break;
|
|
||||||
case 'always':
|
|
||||||
linkTypeValue = i18n._(t`Always`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Title headingLevel="h1" size="xl">
|
|
||||||
{i18n._(t`Workflow Job Template Node`)}
|
|
||||||
</Title>
|
|
||||||
<HorizontalSeparator />
|
|
||||||
<DetailList gutter="sm">
|
|
||||||
<Detail label={i18n._(t`Name`)} value={workflowJobTemplate.name} />
|
|
||||||
<Detail label={i18n._(t`Run`)} value={linkTypeValue} />
|
|
||||||
</DetailList>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n()(WorkflowJobTemplatePreviewStep);
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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);
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
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);
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
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);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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);
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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);
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
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);
|
||||||
@@ -16,6 +16,7 @@ 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 UnsavedChangesModal from './Modals/UnsavedChangesModal';
|
||||||
|
import NodeViewModal from './Modals/NodeViewModal/NodeViewModal';
|
||||||
import {
|
import {
|
||||||
WorkflowApprovalTemplatesAPI,
|
WorkflowApprovalTemplatesAPI,
|
||||||
WorkflowJobTemplatesAPI,
|
WorkflowJobTemplatesAPI,
|
||||||
@@ -69,6 +70,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
const [nodePositions, setNodePositions] = useState(null);
|
const [nodePositions, setNodePositions] = useState(null);
|
||||||
const [nodeToDelete, setNodeToDelete] = useState(null);
|
const [nodeToDelete, setNodeToDelete] = useState(null);
|
||||||
const [nodeToEdit, setNodeToEdit] = useState(null);
|
const [nodeToEdit, setNodeToEdit] = useState(null);
|
||||||
|
const [nodeToView, setNodeToView] = useState(null);
|
||||||
const [addingLink, setAddingLink] = useState(false);
|
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);
|
||||||
@@ -825,6 +827,7 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
onStartAddLinkClick={selectSourceNodeForLinking}
|
onStartAddLinkClick={selectSourceNodeForLinking}
|
||||||
onConfirmAddLinkClick={selectTargetNodeForLinking}
|
onConfirmAddLinkClick={selectTargetNodeForLinking}
|
||||||
onCancelAddLinkClick={cancelNodeLink}
|
onCancelAddLinkClick={cancelNodeLink}
|
||||||
|
onViewNodeClick={setNodeToView}
|
||||||
addingLink={addingLink}
|
addingLink={addingLink}
|
||||||
addLinkSourceNode={addLinkSourceNode}
|
addLinkSourceNode={addLinkSourceNode}
|
||||||
showKey={showKey}
|
showKey={showKey}
|
||||||
@@ -905,6 +908,9 @@ function Visualizer({ history, template, i18n }) {
|
|||||||
onConfirm={() => deleteAllNodes()}
|
onConfirm={() => deleteAllNodes()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{nodeToView && (
|
||||||
|
<NodeViewModal node={nodeToView} onClose={() => setNodeToView(null)} />
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,18 +31,6 @@ const WorkflowSVG = styled.svg`
|
|||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// const KeyWrapper = styled.div`
|
|
||||||
// position: absolute;
|
|
||||||
// right: 20px;
|
|
||||||
// top: 76px;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
// const ToolsWrapper = styled.div`
|
|
||||||
// position: absolute;
|
|
||||||
// right: 200px;
|
|
||||||
// top: 76px;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
function VisualizerGraph({
|
function VisualizerGraph({
|
||||||
links,
|
links,
|
||||||
nodes,
|
nodes,
|
||||||
@@ -56,6 +44,7 @@ function VisualizerGraph({
|
|||||||
onStartAddLinkClick,
|
onStartAddLinkClick,
|
||||||
onConfirmAddLinkClick,
|
onConfirmAddLinkClick,
|
||||||
onCancelAddLinkClick,
|
onCancelAddLinkClick,
|
||||||
|
onViewNodeClick,
|
||||||
addingLink,
|
addingLink,
|
||||||
addLinkSourceNode,
|
addLinkSourceNode,
|
||||||
showKey,
|
showKey,
|
||||||
@@ -310,6 +299,7 @@ function VisualizerGraph({
|
|||||||
onDeleteNodeClick={onDeleteNodeClick}
|
onDeleteNodeClick={onDeleteNodeClick}
|
||||||
onStartAddLinkClick={onStartAddLinkClick}
|
onStartAddLinkClick={onStartAddLinkClick}
|
||||||
onConfirmAddLinkClick={onConfirmAddLinkClick}
|
onConfirmAddLinkClick={onConfirmAddLinkClick}
|
||||||
|
onViewNodeClick={onViewNodeClick}
|
||||||
addingLink={addingLink}
|
addingLink={addingLink}
|
||||||
isAddLinkSourceNode={
|
isAddLinkSourceNode={
|
||||||
addLinkSourceNode && addLinkSourceNode.id === node.id
|
addLinkSourceNode && addLinkSourceNode.id === node.id
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ function VisualizerNode({
|
|||||||
isAddLinkSourceNode,
|
isAddLinkSourceNode,
|
||||||
onAddNodeClick,
|
onAddNodeClick,
|
||||||
onEditNodeClick,
|
onEditNodeClick,
|
||||||
|
onViewNodeClick,
|
||||||
}) {
|
}) {
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
|
|
||||||
@@ -89,6 +90,11 @@ function VisualizerNode({
|
|||||||
key="details"
|
key="details"
|
||||||
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
||||||
onMouseLeave={() => updateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
|
onClick={() => {
|
||||||
|
updateHelpText(null);
|
||||||
|
setHovering(false);
|
||||||
|
onViewNodeClick(node);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
</WorkflowActionTooltipItem>
|
</WorkflowActionTooltipItem>
|
||||||
|
|||||||
Reference in New Issue
Block a user