mirror of
https://github.com/ansible/awx.git
synced 2026-01-22 06:58:06 -03: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:
parent
de55af6ae6
commit
50ba4f9759
@ -51,6 +51,10 @@ class WorkflowJobTemplateNodes extends Base {
|
||||
disassociate: true,
|
||||
});
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowJobTemplateNodes;
|
||||
|
||||
@ -2,10 +2,10 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Wizard } from '@patternfly/react-core';
|
||||
import SelectResourceStep from './SelectResourceStep';
|
||||
import SelectRoleStep from './SelectRoleStep';
|
||||
import { SelectableCard } from '@components/SelectableCard';
|
||||
import { Wizard } from '@components/Wizard';
|
||||
import { TeamsAPI, UsersAPI } from '../../api';
|
||||
|
||||
const readUsers = async queryParams =>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { DetailName, DetailValue } from '@components/DetailList';
|
||||
import { yamlToJson, jsonToYaml, isJson } from '@util/yaml';
|
||||
@ -21,7 +21,7 @@ function getValueAsMode(value, mode) {
|
||||
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 [currentValue, setCurrentValue] = useState(value || '---');
|
||||
const [error, setError] = useState(null);
|
||||
@ -90,7 +90,7 @@ function VariablesDetail({ value, label, rows }) {
|
||||
}
|
||||
VariablesDetail.propTypes = {
|
||||
value: string.isRequired,
|
||||
label: string.isRequired,
|
||||
label: node.isRequired,
|
||||
rows: number,
|
||||
};
|
||||
VariablesDetail.defaultProps = {
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
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.
|
||||
const ContentLoading = ({ className, i18n }) => (
|
||||
|
||||
@ -25,8 +25,15 @@ const DetailValue = styled(({ fullWidth, ...props }) => (
|
||||
`}
|
||||
`;
|
||||
|
||||
const Detail = ({ label, value, fullWidth, className, dataCy }) => {
|
||||
if (!value && typeof value !== 'number') {
|
||||
const Detail = ({
|
||||
label,
|
||||
value,
|
||||
fullWidth,
|
||||
className,
|
||||
dataCy,
|
||||
alwaysVisible,
|
||||
}) => {
|
||||
if (!value && typeof value !== 'number' && !alwaysVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -58,10 +65,12 @@ Detail.propTypes = {
|
||||
label: node.isRequired,
|
||||
value: node,
|
||||
fullWidth: bool,
|
||||
alwaysVisible: bool,
|
||||
};
|
||||
Detail.defaultProps = {
|
||||
value: null,
|
||||
fullWidth: false,
|
||||
alwaysVisible: false,
|
||||
};
|
||||
|
||||
export default Detail;
|
||||
|
||||
@ -10,14 +10,11 @@ import styled from 'styled-components';
|
||||
import VerticalSeparator from '../VerticalSeparator';
|
||||
|
||||
const Split = styled(PFSplit)`
|
||||
padding-top: 15px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: #ebebeb var(--pf-global--BorderWidth--sm) solid;
|
||||
margin: 20px 0px;
|
||||
align-items: baseline;
|
||||
`;
|
||||
|
||||
const SplitLabelItem = styled(SplitItem)`
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
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 { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Button,
|
||||
Wizard,
|
||||
WizardContextConsumer,
|
||||
WizardFooter,
|
||||
} from '@patternfly/react-core';
|
||||
import NodeResourceStep from './NodeResourceStep';
|
||||
import NodeTypeStep from './NodeTypeStep';
|
||||
import NodeTypeStep from './NodeTypeStep/NodeTypeStep';
|
||||
import RunStep from './RunStep';
|
||||
import NodeNextButton from './NodeNextButton';
|
||||
import NodeApprovalStep from './NodeApprovalStep';
|
||||
import ApprovalPreviewStep from './ApprovalPreviewStep';
|
||||
import JobTemplatePreviewStep from './JobTemplatePreviewStep';
|
||||
import InventorySyncPreviewStep from './InventorySyncPreviewStep';
|
||||
import ProjectSyncPreviewStep from './ProjectSyncPreviewStep';
|
||||
import WorkflowJobTemplatePreviewStep from './WorkflowJobTemplatePreviewStep';
|
||||
import { Wizard } from '@components/Wizard';
|
||||
|
||||
import {
|
||||
JobTemplatesAPI,
|
||||
ProjectsAPI,
|
||||
InventorySourcesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '@api';
|
||||
|
||||
const readInventorySources = async queryParams =>
|
||||
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 }) {
|
||||
function NodeModal({
|
||||
history,
|
||||
i18n,
|
||||
title,
|
||||
onClose,
|
||||
onSave,
|
||||
node,
|
||||
askLinkType,
|
||||
}) {
|
||||
let defaultNodeType = 'job_template';
|
||||
let defaultNodeResource = null;
|
||||
let defaultApprovalName = '';
|
||||
@ -82,15 +70,6 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
||||
const [nodeType, setNodeType] = useState(defaultNodeType);
|
||||
const [linkType, setLinkType] = useState('success');
|
||||
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 [approvalName, setApprovalName] = useState(defaultApprovalName);
|
||||
const [approvalDescription, setApprovalDescription] = useState(
|
||||
@ -100,7 +79,19 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
||||
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 = () => {
|
||||
clearQueryParams();
|
||||
|
||||
const resource =
|
||||
nodeType === 'approval'
|
||||
? {
|
||||
@ -120,47 +111,13 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
||||
});
|
||||
};
|
||||
|
||||
const resourceSearch = queryParams => {
|
||||
switch (nodeType) {
|
||||
case 'inventory_source_sync':
|
||||
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 handleCancel = () => {
|
||||
clearQueryParams();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleNodeTypeChange = newNodeType => {
|
||||
setNodeType(newNodeType);
|
||||
setShowResourceStep(false);
|
||||
setShowApprovalStep(false);
|
||||
setShowPreviewStep(false);
|
||||
setNodeResource(null);
|
||||
setApprovalName('');
|
||||
setApprovalDescription('');
|
||||
@ -168,101 +125,39 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
||||
};
|
||||
|
||||
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`),
|
||||
key: 'node_type',
|
||||
name: i18n._(t`Node Type`),
|
||||
key: 'node_resource',
|
||||
enableNext:
|
||||
(nodeType !== 'approval' && nodeResource !== null) ||
|
||||
(nodeType === 'approval' && approvalName !== ''),
|
||||
component: (
|
||||
<NodeTypeStep
|
||||
nodeType={nodeType}
|
||||
updateNodeType={handleNodeTypeChange}
|
||||
askLinkType={askLinkType}
|
||||
linkType={linkType}
|
||||
updateLinkType={setLinkType}
|
||||
nodeResource={nodeResource}
|
||||
updateNodeResource={setNodeResource}
|
||||
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) => {
|
||||
@ -272,20 +167,25 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
||||
const CustomFooter = (
|
||||
<WizardFooter>
|
||||
<WizardContextConsumer>
|
||||
{({ activeStep, onNext, onBack, onClose }) => (
|
||||
{({ activeStep, onNext, onBack }) => (
|
||||
<>
|
||||
<NodeNextButton
|
||||
triggerNext={triggerNext}
|
||||
activeStep={activeStep}
|
||||
onNext={onNext}
|
||||
onClick={handleNextClick}
|
||||
onClick={() => setTriggerNext(triggerNext + 1)}
|
||||
buttonText={
|
||||
activeStep.key === 'node_resource'
|
||||
? i18n._(t`Save`)
|
||||
: i18n._(t`Next`)
|
||||
}
|
||||
/>
|
||||
{activeStep && activeStep.id !== 1 && (
|
||||
<Button variant="secondary" onClick={onBack}>
|
||||
{i18n._(t`Back`)}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="link" onClick={onClose}>
|
||||
<Button variant="link" onClick={handleCancel}>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>
|
||||
</>
|
||||
@ -294,17 +194,19 @@ function NodeModal({ i18n, title, onClose, onSave, node, askLinkType }) {
|
||||
</WizardFooter>
|
||||
);
|
||||
|
||||
const wizardTitle = nodeResource ? `${title} | ${nodeResource.name}` : title;
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
style={{ overflow: 'scroll' }}
|
||||
isOpen
|
||||
steps={steps}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
title={wizardTitle}
|
||||
onClose={handleCancel}
|
||||
onSave={handleSaveNode}
|
||||
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 { Button } from '@patternfly/react-core';
|
||||
|
||||
function NodeNextButton({ i18n, activeStep, onNext, triggerNext, onClick }) {
|
||||
function NodeNextButton({
|
||||
i18n,
|
||||
activeStep,
|
||||
onNext,
|
||||
triggerNext,
|
||||
onClick,
|
||||
buttonText,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!triggerNext) {
|
||||
return;
|
||||
@ -18,7 +25,7 @@ function NodeNextButton({ i18n, activeStep, onNext, triggerNext, onClick }) {
|
||||
onClick={() => onClick(activeStep)}
|
||||
isDisabled={!activeStep.enableNext}
|
||||
>
|
||||
{activeStep.key === 'preview' ? i18n._(t`Save`) : i18n._(t`Next`)}
|
||||
{buttonText}
|
||||
</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 VisualizerToolbar from './VisualizerToolbar';
|
||||
import UnsavedChangesModal from './Modals/UnsavedChangesModal';
|
||||
import NodeViewModal from './Modals/NodeViewModal/NodeViewModal';
|
||||
import {
|
||||
WorkflowApprovalTemplatesAPI,
|
||||
WorkflowJobTemplatesAPI,
|
||||
@ -69,6 +70,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
const [nodePositions, setNodePositions] = useState(null);
|
||||
const [nodeToDelete, setNodeToDelete] = useState(null);
|
||||
const [nodeToEdit, setNodeToEdit] = useState(null);
|
||||
const [nodeToView, setNodeToView] = useState(null);
|
||||
const [addingLink, setAddingLink] = useState(false);
|
||||
const [addLinkSourceNode, setAddLinkSourceNode] = useState(null);
|
||||
const [addLinkTargetNode, setAddLinkTargetNode] = useState(null);
|
||||
@ -825,6 +827,7 @@ function Visualizer({ history, template, i18n }) {
|
||||
onStartAddLinkClick={selectSourceNodeForLinking}
|
||||
onConfirmAddLinkClick={selectTargetNodeForLinking}
|
||||
onCancelAddLinkClick={cancelNodeLink}
|
||||
onViewNodeClick={setNodeToView}
|
||||
addingLink={addingLink}
|
||||
addLinkSourceNode={addLinkSourceNode}
|
||||
showKey={showKey}
|
||||
@ -905,6 +908,9 @@ function Visualizer({ history, template, i18n }) {
|
||||
onConfirm={() => deleteAllNodes()}
|
||||
/>
|
||||
)}
|
||||
{nodeToView && (
|
||||
<NodeViewModal node={nodeToView} onClose={() => setNodeToView(null)} />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -31,18 +31,6 @@ const WorkflowSVG = styled.svg`
|
||||
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({
|
||||
links,
|
||||
nodes,
|
||||
@ -56,6 +44,7 @@ function VisualizerGraph({
|
||||
onStartAddLinkClick,
|
||||
onConfirmAddLinkClick,
|
||||
onCancelAddLinkClick,
|
||||
onViewNodeClick,
|
||||
addingLink,
|
||||
addLinkSourceNode,
|
||||
showKey,
|
||||
@ -310,6 +299,7 @@ function VisualizerGraph({
|
||||
onDeleteNodeClick={onDeleteNodeClick}
|
||||
onStartAddLinkClick={onStartAddLinkClick}
|
||||
onConfirmAddLinkClick={onConfirmAddLinkClick}
|
||||
onViewNodeClick={onViewNodeClick}
|
||||
addingLink={addingLink}
|
||||
isAddLinkSourceNode={
|
||||
addLinkSourceNode && addLinkSourceNode.id === node.id
|
||||
|
||||
@ -51,6 +51,7 @@ function VisualizerNode({
|
||||
isAddLinkSourceNode,
|
||||
onAddNodeClick,
|
||||
onEditNodeClick,
|
||||
onViewNodeClick,
|
||||
}) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
||||
@ -89,6 +90,11 @@ function VisualizerNode({
|
||||
key="details"
|
||||
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
||||
onMouseLeave={() => updateHelpText(null)}
|
||||
onClick={() => {
|
||||
updateHelpText(null);
|
||||
setHovering(false);
|
||||
onViewNodeClick(node);
|
||||
}}
|
||||
>
|
||||
<InfoIcon />
|
||||
</WorkflowActionTooltipItem>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user