mirror of
https://github.com/ansible/awx.git
synced 2026-03-03 09:48:51 -03:30
Merge pull request #5520 from keithjgrant/5261-inventory-detail-b
Inventory Detail Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -296,7 +296,7 @@ The lingui library provides various React helpers for dealing with both marking
|
|||||||
|
|
||||||
**Note:** Variables that are put inside the t-marked template tag will not be translated. If you have a variable string with text that needs translating, you must wrap it in ```i18n._(t``)``` where it is defined.
|
**Note:** Variables that are put inside the t-marked template tag will not be translated. If you have a variable string with text that needs translating, you must wrap it in ```i18n._(t``)``` where it is defined.
|
||||||
|
|
||||||
**Note:** We do not use the `I18n` consumer, `i18nMark` function, or `<Trans>` component lingui gives us access to in this repo. i18nMark does not actually replace the string in the UI (leading to the potential for untranslated bugs), and the other helpers are redundant. Settling on a consistent, single pattern helps us ease the mental overhead of the need to understand the ins and outs of the lingui API.
|
**Note:** We try to avoid the `I18n` consumer, `i18nMark` function, or `<Trans>` component lingui gives us access to in this repo. i18nMark does not actually replace the string in the UI (leading to the potential for untranslated bugs), and the other helpers are redundant. Settling on a consistent, single pattern helps us ease the mental overhead of the need to understand the ins and outs of the lingui API.
|
||||||
|
|
||||||
You can learn more about the ways lingui and its React helpers at [this link](https://lingui.js.org/tutorials/react-patterns.html).
|
You can learn more about the ways lingui and its React helpers at [this link](https://lingui.js.org/tutorials/react-patterns.html).
|
||||||
|
|
||||||
|
|||||||
@@ -156,13 +156,6 @@
|
|||||||
// and bem style, as well as moved into component-based scss files
|
// and bem style, as well as moved into component-based scss files
|
||||||
//
|
//
|
||||||
|
|
||||||
.at-c-listCardBody {
|
|
||||||
--pf-c-card__footer--PaddingX: 0;
|
|
||||||
--pf-c-card__footer--PaddingY: 0;
|
|
||||||
--pf-c-card__body--PaddingX: 0;
|
|
||||||
--pf-c-card__body--PaddingY: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.awx-c-card {
|
.awx-c-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
23
awx/ui_next/src/components/Card/CardActionsRow.jsx
Normal file
23
awx/ui_next/src/components/Card/CardActionsRow.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CardActions } from '@patternfly/react-core';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const CardActionsWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
& > .pf-c-card__actions > :not(:first-child) {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function CardActionsRow({ children }) {
|
||||||
|
return (
|
||||||
|
<CardActionsWrapper>
|
||||||
|
<CardActions>{children}</CardActions>
|
||||||
|
</CardActionsWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardActionsRow;
|
||||||
9
awx/ui_next/src/components/Card/CardBody.jsx
Normal file
9
awx/ui_next/src/components/Card/CardBody.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { CardBody } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const TabbedCardBody = styled(CardBody)`
|
||||||
|
padding-top: var(--pf-c-card--first-child--PaddingTop);
|
||||||
|
`;
|
||||||
|
CardBody.displayName = 'PFCardBody';
|
||||||
|
|
||||||
|
export default TabbedCardBody;
|
||||||
13
awx/ui_next/src/components/Card/TabbedCardHeader.js
Normal file
13
awx/ui_next/src/components/Card/TabbedCardHeader.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { CardHeader } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const TabbedCardHeader = styled(CardHeader)`
|
||||||
|
--pf-c-card--first-child--PaddingTop: 0;
|
||||||
|
--pf-c-card--child--PaddingLeft: 0;
|
||||||
|
--pf-c-card--child--PaddingRight: 0;
|
||||||
|
--pf-c-card__header--not-last-child--PaddingBottom: 24px;
|
||||||
|
--pf-c-card__header--not-last-child--PaddingBottom: 0;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default TabbedCardHeader;
|
||||||
3
awx/ui_next/src/components/Card/index.js
Normal file
3
awx/ui_next/src/components/Card/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as TabbedCardHeader } from './TabbedCardHeader';
|
||||||
|
export { default as CardBody } from './CardBody';
|
||||||
|
export { default as CardActionsRow } from './CardActionsRow';
|
||||||
@@ -83,7 +83,7 @@ function CodeMirrorInput({
|
|||||||
}
|
}
|
||||||
CodeMirrorInput.propTypes = {
|
CodeMirrorInput.propTypes = {
|
||||||
value: string.isRequired,
|
value: string.isRequired,
|
||||||
onChange: func.isRequired,
|
onChange: func,
|
||||||
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
|
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
|
||||||
readOnly: bool,
|
readOnly: bool,
|
||||||
hasErrors: bool,
|
hasErrors: bool,
|
||||||
@@ -91,6 +91,7 @@ CodeMirrorInput.propTypes = {
|
|||||||
};
|
};
|
||||||
CodeMirrorInput.defaultProps = {
|
CodeMirrorInput.defaultProps = {
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
onChange: () => {},
|
||||||
rows: 6,
|
rows: 6,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { string, number } from 'prop-types';
|
||||||
|
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
|
||||||
|
import { DetailName, DetailValue } from '@components/DetailList';
|
||||||
|
import CodeMirrorInput from './CodeMirrorInput';
|
||||||
|
import YamlJsonToggle from './YamlJsonToggle';
|
||||||
|
import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml';
|
||||||
|
|
||||||
|
const YAML_MODE = 'yaml';
|
||||||
|
const JSON_MODE = 'javascript';
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DetailName
|
||||||
|
component={TextListItemVariants.dt}
|
||||||
|
fullWidth
|
||||||
|
css="grid-column: 1 / -1"
|
||||||
|
>
|
||||||
|
<Split gutter="sm">
|
||||||
|
<SplitItem>
|
||||||
|
<div className="pf-c-form__label">
|
||||||
|
<span
|
||||||
|
className="pf-c-form__label-text"
|
||||||
|
css="font-weight: var(--pf-global--FontWeight--bold)"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem>
|
||||||
|
<YamlJsonToggle
|
||||||
|
mode={mode}
|
||||||
|
onChange={newMode => {
|
||||||
|
try {
|
||||||
|
const newVal =
|
||||||
|
newMode === YAML_MODE
|
||||||
|
? jsonToYaml(currentValue)
|
||||||
|
: yamlToJson(currentValue);
|
||||||
|
setCurrentValue(newVal);
|
||||||
|
setMode(newMode);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SplitItem>
|
||||||
|
</Split>
|
||||||
|
</DetailName>
|
||||||
|
<DetailValue
|
||||||
|
component={TextListItemVariants.dd}
|
||||||
|
fullWidth
|
||||||
|
css="grid-column: 1 / -1; margin-top: -20px"
|
||||||
|
>
|
||||||
|
<CodeMirrorInput
|
||||||
|
mode={mode}
|
||||||
|
value={currentValue}
|
||||||
|
readOnly
|
||||||
|
rows={rows}
|
||||||
|
css="margin-top: 10px"
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
css="color: var(--pf-global--danger-color--100);
|
||||||
|
font-size: var(--pf-global--FontSize--sm"
|
||||||
|
>
|
||||||
|
Error: {error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DetailValue>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
VariablesDetail.propTypes = {
|
||||||
|
value: string.isRequired,
|
||||||
|
label: string.isRequired,
|
||||||
|
rows: number,
|
||||||
|
};
|
||||||
|
VariablesDetail.defaultProps = {
|
||||||
|
rows: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VariablesDetail;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import VariablesDetail from './VariablesDetail';
|
||||||
|
|
||||||
|
jest.mock('@api');
|
||||||
|
|
||||||
|
describe('<VariablesDetail>', () => {
|
||||||
|
test('should render readonly CodeMirrorInput', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<VariablesDetail value="---foo: bar" label="Variables" />
|
||||||
|
);
|
||||||
|
const input = wrapper.find('Styled(CodeMirrorInput)');
|
||||||
|
expect(input).toHaveLength(1);
|
||||||
|
expect(input.prop('mode')).toEqual('yaml');
|
||||||
|
expect(input.prop('value')).toEqual('---foo: bar');
|
||||||
|
expect(input.prop('readOnly')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect JSON', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<VariablesDetail value='{"foo": "bar"}' label="Variables" />
|
||||||
|
);
|
||||||
|
const input = wrapper.find('Styled(CodeMirrorInput)');
|
||||||
|
expect(input).toHaveLength(1);
|
||||||
|
expect(input.prop('mode')).toEqual('javascript');
|
||||||
|
expect(input.prop('value')).toEqual('{"foo": "bar"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should convert between modes', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<VariablesDetail value="---foo: bar" label="Variables" />
|
||||||
|
);
|
||||||
|
wrapper.find('YamlJsonToggle').invoke('onChange')('javascript');
|
||||||
|
const input = wrapper.find('Styled(CodeMirrorInput)');
|
||||||
|
expect(input.prop('mode')).toEqual('javascript');
|
||||||
|
expect(input.prop('value')).toEqual('{\n "foo": "bar"\n}');
|
||||||
|
|
||||||
|
wrapper.find('YamlJsonToggle').invoke('onChange')('yaml');
|
||||||
|
const input2 = wrapper.find('Styled(CodeMirrorInput)');
|
||||||
|
expect(input2.prop('mode')).toEqual('yaml');
|
||||||
|
expect(input2.prop('value')).toEqual('foo: bar\n');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { string, bool } from 'prop-types';
|
import { string, bool } from 'prop-types';
|
||||||
import { Field } from 'formik';
|
import { Field } from 'formik';
|
||||||
import { Button, Split, SplitItem } from '@patternfly/react-core';
|
import { Split, SplitItem } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
|
||||||
import ButtonGroup from '../ButtonGroup';
|
|
||||||
import CodeMirrorInput from './CodeMirrorInput';
|
import CodeMirrorInput from './CodeMirrorInput';
|
||||||
|
import YamlJsonToggle from './YamlJsonToggle';
|
||||||
import { yamlToJson, jsonToYaml } from '../../util/yaml';
|
import { yamlToJson, jsonToYaml } from '../../util/yaml';
|
||||||
|
|
||||||
const YAML_MODE = 'yaml';
|
const YAML_MODE = 'yaml';
|
||||||
const JSON_MODE = 'javascript';
|
|
||||||
|
|
||||||
const SmallButton = styled(Button)`
|
|
||||||
padding: 3px 8px;
|
|
||||||
font-size: var(--pf-global--FontSize--xs);
|
|
||||||
`;
|
|
||||||
|
|
||||||
function VariablesField({ id, name, label, readOnly }) {
|
function VariablesField({ id, name, label, readOnly }) {
|
||||||
|
// TODO: detect initial mode
|
||||||
const [mode, setMode] = useState(YAML_MODE);
|
const [mode, setMode] = useState(YAML_MODE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -30,40 +24,21 @@ function VariablesField({ id, name, label, readOnly }) {
|
|||||||
</label>
|
</label>
|
||||||
</SplitItem>
|
</SplitItem>
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<ButtonGroup>
|
<YamlJsonToggle
|
||||||
<SmallButton
|
mode={mode}
|
||||||
onClick={() => {
|
onChange={newMode => {
|
||||||
if (mode === YAML_MODE) {
|
try {
|
||||||
return;
|
const newVal =
|
||||||
}
|
newMode === YAML_MODE
|
||||||
try {
|
? jsonToYaml(field.value)
|
||||||
form.setFieldValue(name, jsonToYaml(field.value));
|
: yamlToJson(field.value);
|
||||||
setMode(YAML_MODE);
|
form.setFieldValue(name, newVal);
|
||||||
} catch (err) {
|
setMode(newMode);
|
||||||
form.setFieldError(name, err.message);
|
} catch (err) {
|
||||||
}
|
form.setFieldError(name, err.message);
|
||||||
}}
|
}
|
||||||
variant={mode === YAML_MODE ? 'primary' : 'secondary'}
|
}}
|
||||||
>
|
/>
|
||||||
YAML
|
|
||||||
</SmallButton>
|
|
||||||
<SmallButton
|
|
||||||
onClick={() => {
|
|
||||||
if (mode === JSON_MODE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
form.setFieldValue(name, yamlToJson(field.value));
|
|
||||||
setMode(JSON_MODE);
|
|
||||||
} catch (err) {
|
|
||||||
form.setFieldError(name, err.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={mode === JSON_MODE ? 'primary' : 'secondary'}
|
|
||||||
>
|
|
||||||
JSON
|
|
||||||
</SmallButton>
|
|
||||||
</ButtonGroup>
|
|
||||||
</SplitItem>
|
</SplitItem>
|
||||||
</Split>
|
</Split>
|
||||||
<CodeMirrorInput
|
<CodeMirrorInput
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { oneOf, func } from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
import ButtonGroup from '../ButtonGroup';
|
||||||
|
|
||||||
|
const SmallButton = styled(Button)`
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: var(--pf-global--FontSize--xs);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const YAML_MODE = 'yaml';
|
||||||
|
const JSON_MODE = 'javascript';
|
||||||
|
|
||||||
|
function YamlJsonToggle({ mode, onChange }) {
|
||||||
|
const setMode = newMode => {
|
||||||
|
if (mode !== newMode) {
|
||||||
|
onChange(newMode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup>
|
||||||
|
<SmallButton
|
||||||
|
onClick={() => setMode(YAML_MODE)}
|
||||||
|
variant={mode === YAML_MODE ? 'primary' : 'secondary'}
|
||||||
|
>
|
||||||
|
YAML
|
||||||
|
</SmallButton>
|
||||||
|
<SmallButton
|
||||||
|
onClick={() => setMode(JSON_MODE)}
|
||||||
|
variant={mode === JSON_MODE ? 'primary' : 'secondary'}
|
||||||
|
>
|
||||||
|
JSON
|
||||||
|
</SmallButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
YamlJsonToggle.propTypes = {
|
||||||
|
mode: oneOf([YAML_MODE, JSON_MODE]).isRequired,
|
||||||
|
onChange: func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default YamlJsonToggle;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import CodeMirrorInput from './CodeMirrorInput';
|
import CodeMirrorInput from './CodeMirrorInput';
|
||||||
|
|
||||||
export default CodeMirrorInput;
|
export default CodeMirrorInput;
|
||||||
|
export { default as VariablesDetail } from './VariablesDetail';
|
||||||
export { default as VariablesInput } from './VariablesInput';
|
export { default as VariablesInput } from './VariablesInput';
|
||||||
export { default as VariablesField } from './VariablesField';
|
export { default as VariablesField } from './VariablesField';
|
||||||
|
|||||||
50
awx/ui_next/src/components/DeleteButton/DeleteButton.jsx
Normal file
50
awx/ui_next/src/components/DeleteButton/DeleteButton.jsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
import AlertModal from '@components/AlertModal';
|
||||||
|
import { CardActionsRow } from '@components/Card';
|
||||||
|
|
||||||
|
function DeleteButton({ onConfirm, modalTitle, name, i18n }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
aria-label={i18n._(t`Delete`)}
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</Button>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={modalTitle}
|
||||||
|
variant="danger"
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Are you sure you want to delete:`)}
|
||||||
|
<br />
|
||||||
|
<strong>{name}</strong>
|
||||||
|
<CardActionsRow>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
aria-label={i18n._(t`Cancel`)}
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Cancel`)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
aria-label={i18n._(t`Delete`)}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</Button>
|
||||||
|
</CardActionsRow>
|
||||||
|
</AlertModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(DeleteButton);
|
||||||
1
awx/ui_next/src/components/DeleteButton/index.js
Normal file
1
awx/ui_next/src/components/DeleteButton/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './DeleteButton';
|
||||||
36
awx/ui_next/src/components/DetailList/UserDateDetail.jsx
Normal file
36
awx/ui_next/src/components/DetailList/UserDateDetail.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { node, string } from 'prop-types';
|
||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { formatDateString } from '@util/dates';
|
||||||
|
import Detail from './Detail';
|
||||||
|
import { SummaryFieldUser } from '../../types';
|
||||||
|
|
||||||
|
function UserDateDetail({ label, date, user }) {
|
||||||
|
const dateStr = formatDateString(date);
|
||||||
|
const username = user ? user.username : '';
|
||||||
|
return (
|
||||||
|
<Detail
|
||||||
|
label={label}
|
||||||
|
value={
|
||||||
|
user ? (
|
||||||
|
<Trans>
|
||||||
|
{dateStr} by <Link to={`/users/${user.id}`}>{username}</Link>
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
dateStr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
UserDateDetail.propTypes = {
|
||||||
|
label: node.isRequired,
|
||||||
|
date: string.isRequired,
|
||||||
|
user: SummaryFieldUser,
|
||||||
|
};
|
||||||
|
UserDateDetail.defaultProps = {
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserDateDetail;
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { default as DetailList } from './DetailList';
|
export { default as DetailList } from './DetailList';
|
||||||
export { default as Detail, DetailName, DetailValue } from './Detail';
|
export { default as Detail, DetailName, DetailValue } from './Detail';
|
||||||
|
export { default as UserDateDetail } from './UserDateDetail';
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ import React, { Component } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||||
import {
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
Card,
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
CardHeader as PFCardHeader,
|
|
||||||
PageSection,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
@@ -18,13 +14,6 @@ import HostGroups from './HostGroups';
|
|||||||
import HostCompletedJobs from './HostCompletedJobs';
|
import HostCompletedJobs from './HostCompletedJobs';
|
||||||
import { HostsAPI } from '@api';
|
import { HostsAPI } from '@api';
|
||||||
|
|
||||||
const CardHeader = styled(PFCardHeader)`
|
|
||||||
--pf-c-card--first-child--PaddingTop: 0;
|
|
||||||
--pf-c-card--child--PaddingLeft: 0;
|
|
||||||
--pf-c-card--child--PaddingRight: 0;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class Host extends Component {
|
class Host extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -89,7 +78,7 @@ class Host extends Component {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs
|
<RoutedTabs
|
||||||
match={match}
|
match={match}
|
||||||
history={history}
|
history={history}
|
||||||
@@ -97,7 +86,7 @@ class Host extends Component {
|
|||||||
tabsArray={tabsArray}
|
tabsArray={tabsArray}
|
||||||
/>
|
/>
|
||||||
<CardCloseButton linkTo="/hosts" />
|
<CardCloseButton linkTo="/hosts" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { PageSection, Card, CardBody } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import { HostsAPI } from '@api';
|
import { HostsAPI } from '@api';
|
||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
import HostForm from '../shared';
|
import HostForm from '../shared';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class HostCompletedJobs extends Component {
|
class HostCompletedJobs extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Host } from '@types';
|
import { Host } from '@types';
|
||||||
import { formatDateString } from '@util/dates';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { Button, CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||||
import CodeMirrorInput from '@components/CodeMirrorInput';
|
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||||
|
|
||||||
const ActionButtonWrapper = styled.div`
|
const ActionButtonWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -21,32 +21,8 @@ const ActionButtonWrapper = styled.div`
|
|||||||
function HostDetail({ host, i18n }) {
|
function HostDetail({ host, i18n }) {
|
||||||
const { created, description, id, modified, name, summary_fields } = host;
|
const { created, description, id, modified, name, summary_fields } = host;
|
||||||
|
|
||||||
let createdBy = '';
|
|
||||||
if (created) {
|
|
||||||
if (summary_fields.created_by && summary_fields.created_by.username) {
|
|
||||||
createdBy = i18n._(
|
|
||||||
t`${formatDateString(created)} by ${summary_fields.created_by.username}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
createdBy = formatDateString(created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let modifiedBy = '';
|
|
||||||
if (modified) {
|
|
||||||
if (summary_fields.modified_by && summary_fields.modified_by.username) {
|
|
||||||
modifiedBy = i18n._(
|
|
||||||
t`${formatDateString(modified)} by ${
|
|
||||||
summary_fields.modified_by.username
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
modifiedBy = formatDateString(modified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody css="padding-top: 20px">
|
<CardBody>
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
<Detail label={i18n._(t`Name`)} value={name} />
|
<Detail label={i18n._(t`Name`)} value={name} />
|
||||||
<Detail label={i18n._(t`Description`)} value={description} />
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
@@ -66,23 +42,20 @@ function HostDetail({ host, i18n }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* TODO: Link to user in users */}
|
<UserDateDetail
|
||||||
<Detail label={i18n._(t`Created`)} value={createdBy} />
|
label={i18n._(t`Created`)}
|
||||||
{/* TODO: Link to user in users */}
|
date={created}
|
||||||
<Detail label={i18n._(t`Last Modified`)} value={modifiedBy} />
|
user={summary_fields.created_by}
|
||||||
<Detail
|
/>
|
||||||
fullWidth
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Last Modified`)}
|
||||||
|
date={modified}
|
||||||
|
user={summary_fields.modified_by}
|
||||||
|
/>
|
||||||
|
<VariablesDetail
|
||||||
label={i18n._(t`Variables`)}
|
label={i18n._(t`Variables`)}
|
||||||
value={
|
value={host.variables}
|
||||||
<CodeMirrorInput
|
rows={6}
|
||||||
mode="yaml"
|
|
||||||
readOnly
|
|
||||||
value={host.variables}
|
|
||||||
onChange={() => {}}
|
|
||||||
rows={6}
|
|
||||||
hasErrors={false}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<ActionButtonWrapper>
|
<ActionButtonWrapper>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe('<HostDetail />', () => {
|
|||||||
mountWithContexts(<HostDetail host={mockHost} />);
|
mountWithContexts(<HostDetail host={mockHost} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render Details', async done => {
|
test('should render Details', async () => {
|
||||||
const wrapper = mountWithContexts(<HostDetail host={mockHost} />);
|
const wrapper = mountWithContexts(<HostDetail host={mockHost} />);
|
||||||
const testParams = [
|
const testParams = [
|
||||||
{ label: 'Name', value: 'Foo' },
|
{ label: 'Name', value: 'Foo' },
|
||||||
@@ -46,23 +46,22 @@ describe('<HostDetail />', () => {
|
|||||||
expect(detail.find('dt').text()).toBe(label);
|
expect(detail.find('dt').text()).toBe(label);
|
||||||
expect(detail.find('dd').text()).toBe(value);
|
expect(detail.find('dd').text()).toBe(value);
|
||||||
}
|
}
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show edit button for users with edit permission', async done => {
|
test('should show edit button for users with edit permission', async () => {
|
||||||
const wrapper = mountWithContexts(<HostDetail host={mockHost} />);
|
const wrapper = mountWithContexts(<HostDetail host={mockHost} />);
|
||||||
const editButton = await waitForElement(wrapper, 'HostDetail Button');
|
// VariablesDetail has two buttons
|
||||||
|
const editButton = wrapper.find('Button').at(2);
|
||||||
expect(editButton.text()).toEqual('Edit');
|
expect(editButton.text()).toEqual('Edit');
|
||||||
expect(editButton.prop('to')).toBe('/hosts/1/edit');
|
expect(editButton.prop('to')).toBe('/hosts/1/edit');
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should hide edit button for users without edit permission', async done => {
|
test('should hide edit button for users without edit permission', async () => {
|
||||||
const readOnlyHost = { ...mockHost };
|
const readOnlyHost = { ...mockHost };
|
||||||
readOnlyHost.summary_fields.user_capabilities.edit = false;
|
readOnlyHost.summary_fields.user_capabilities.edit = false;
|
||||||
const wrapper = mountWithContexts(<HostDetail host={readOnlyHost} />);
|
const wrapper = mountWithContexts(<HostDetail host={readOnlyHost} />);
|
||||||
await waitForElement(wrapper, 'HostDetail');
|
await waitForElement(wrapper, 'HostDetail');
|
||||||
expect(wrapper.find('HostDetail Button').length).toBe(0);
|
// VariablesDetail has two buttons
|
||||||
done();
|
expect(wrapper.find('Button').length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
import { HostsAPI } from '@api';
|
import { HostsAPI } from '@api';
|
||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class HostFacts extends Component {
|
class HostFacts extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class HostGroups extends Component {
|
class HostGroups extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
||||||
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
@@ -51,10 +52,10 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let cardHeader = hasContentLoading ? null : (
|
let cardHeader = hasContentLoading ? null : (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||||
<CardCloseButton linkTo="/inventories" />
|
<CardCloseButton linkTo="/inventories" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -2,14 +2,8 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import { PageSection, Card, CardHeader, Tooltip } from '@patternfly/react-core';
|
||||||
PageSection,
|
import { CardBody } from '@components/Card';
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class InventoryCompletedJobs extends Component {
|
class InventoryCompletedJobs extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,10 +1,124 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
import { CardBody, CardActionsRow } from '@components/Card';
|
||||||
|
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||||
|
import { ChipGroup, Chip } from '@components/Chip';
|
||||||
|
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||||
|
import DeleteButton from '@components/DeleteButton';
|
||||||
|
import ContentError from '@components/ContentError';
|
||||||
|
import ContentLoading from '@components/ContentLoading';
|
||||||
|
import { InventoriesAPI } from '@api';
|
||||||
|
import { Inventory } from '../../../types';
|
||||||
|
|
||||||
class InventoryDetail extends Component {
|
function InventoryDetail({ inventory, i18n }) {
|
||||||
render() {
|
const [instanceGroups, setInstanceGroups] = useState([]);
|
||||||
return <CardBody>Coming soon :)</CardBody>;
|
const [hasContentLoading, setHasContentLoading] = useState(true);
|
||||||
|
const [contentError, setContentError] = useState(null);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setHasContentLoading(true);
|
||||||
|
try {
|
||||||
|
const { data } = await InventoriesAPI.readInstanceGroups(inventory.id);
|
||||||
|
setInstanceGroups(data.results);
|
||||||
|
} catch (err) {
|
||||||
|
setContentError(err);
|
||||||
|
} finally {
|
||||||
|
setHasContentLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [inventory.id]);
|
||||||
|
|
||||||
|
const deleteInventory = async () => {
|
||||||
|
await InventoriesAPI.destroy(inventory.id);
|
||||||
|
history.push(`/inventories`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
organization,
|
||||||
|
user_capabilities: userCapabilities,
|
||||||
|
} = inventory.summary_fields;
|
||||||
|
|
||||||
|
if (hasContentLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default InventoryDetail;
|
if (contentError) {
|
||||||
|
return <ContentError error={contentError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<DetailList>
|
||||||
|
<Detail label={i18n._(t`Name`)} value={inventory.name} />
|
||||||
|
<Detail label={i18n._(t`Activity`)} value="Coming soon" />
|
||||||
|
<Detail label={i18n._(t`Description`)} value={inventory.description} />
|
||||||
|
<Detail label={i18n._(t`Type`)} value={i18n._(t`Inventory`)} />
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Organization`)}
|
||||||
|
value={
|
||||||
|
<Link to={`/organizations/${organization.id}/details`}>
|
||||||
|
{organization.name}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={i18n._(t`Instance Groups`)}
|
||||||
|
value={
|
||||||
|
<ChipGroup numChips={5}>
|
||||||
|
{instanceGroups.map(ig => (
|
||||||
|
<Chip key={ig.id} isReadOnly>
|
||||||
|
{ig.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<VariablesDetail
|
||||||
|
label={i18n._(t`Variables`)}
|
||||||
|
value={inventory.variables}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Created`)}
|
||||||
|
date={inventory.created}
|
||||||
|
user={inventory.summary_fields.created_by}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Last Modified`)}
|
||||||
|
date={inventory.modified}
|
||||||
|
user={inventory.summary_fields.modified_by}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
<CardActionsRow>
|
||||||
|
{userCapabilities.edit && (
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={`/inventories/inventory/${inventory.id}/edit`}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{userCapabilities.delete && (
|
||||||
|
<DeleteButton
|
||||||
|
name={inventory.name}
|
||||||
|
modalTitle={i18n._(t`Delete Inventory`)}
|
||||||
|
onConfirm={deleteInventory}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DeleteButton>
|
||||||
|
)}
|
||||||
|
</CardActionsRow>
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
InventoryDetail.propTypes = {
|
||||||
|
inventory: Inventory.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(InventoryDetail);
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import { InventoriesAPI, CredentialTypesAPI } from '@api';
|
||||||
|
import InventoryDetail from './InventoryDetail';
|
||||||
|
|
||||||
|
jest.mock('@api');
|
||||||
|
|
||||||
|
const mockInventory = {
|
||||||
|
id: 1,
|
||||||
|
type: 'inventory',
|
||||||
|
url: '/api/v2/inventories/1/',
|
||||||
|
summary_fields: {
|
||||||
|
organization: {
|
||||||
|
id: 1,
|
||||||
|
name: 'The Organization',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
delete: true,
|
||||||
|
copy: true,
|
||||||
|
adhoc: true,
|
||||||
|
},
|
||||||
|
insights_credential: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2019-10-04T16:56:48.025455Z',
|
||||||
|
modified: '2019-10-04T16:56:48.025468Z',
|
||||||
|
name: 'Inv no hosts',
|
||||||
|
description: '',
|
||||||
|
organization: 1,
|
||||||
|
kind: '',
|
||||||
|
host_filter: null,
|
||||||
|
variables: '---\nfoo: bar',
|
||||||
|
has_active_failures: false,
|
||||||
|
total_hosts: 0,
|
||||||
|
hosts_with_active_failures: 0,
|
||||||
|
total_groups: 0,
|
||||||
|
groups_with_active_failures: 0,
|
||||||
|
has_inventory_sources: false,
|
||||||
|
total_inventory_sources: 0,
|
||||||
|
inventory_sources_with_failures: 0,
|
||||||
|
insights_credential: null,
|
||||||
|
pending_deletion: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
CredentialTypesAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
name: 'insights',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const associatedInstanceGroups = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
InventoriesAPI.readInstanceGroups.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: associatedInstanceGroups,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function expectDetailToMatch(wrapper, label, value) {
|
||||||
|
const detail = wrapper.find(`Detail[label="${label}"]`);
|
||||||
|
expect(detail).toHaveLength(1);
|
||||||
|
expect(detail.prop('value')).toEqual(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('<InventoryDetail />', () => {
|
||||||
|
test('should render details', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InventoryDetail inventory={mockInventory} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expectDetailToMatch(wrapper, 'Name', mockInventory.name);
|
||||||
|
expectDetailToMatch(wrapper, 'Activity', 'Coming soon');
|
||||||
|
expectDetailToMatch(wrapper, 'Description', mockInventory.description);
|
||||||
|
expectDetailToMatch(wrapper, 'Type', 'Inventory');
|
||||||
|
const org = wrapper.find('Detail[label="Organization"]');
|
||||||
|
expect(org.prop('value')).toMatchInlineSnapshot(`
|
||||||
|
<ForwardRef
|
||||||
|
to="/organizations/1/details"
|
||||||
|
>
|
||||||
|
The Organization
|
||||||
|
</ForwardRef>
|
||||||
|
`);
|
||||||
|
const vars = wrapper.find('VariablesDetail');
|
||||||
|
expect(vars).toHaveLength(1);
|
||||||
|
expect(vars.prop('value')).toEqual(mockInventory.variables);
|
||||||
|
const dates = wrapper.find('UserDateDetail');
|
||||||
|
expect(dates).toHaveLength(2);
|
||||||
|
expect(dates.at(0).prop('date')).toEqual(mockInventory.created);
|
||||||
|
expect(dates.at(1).prop('date')).toEqual(mockInventory.modified);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load instance groups', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<InventoryDetail inventory={mockInventory} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledWith(
|
||||||
|
mockInventory.id
|
||||||
|
);
|
||||||
|
const chip = wrapper.find('Chip').at(0);
|
||||||
|
expect(chip.prop('isReadOnly')).toEqual(true);
|
||||||
|
expect(chip.prop('children')).toEqual('Foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,9 +2,10 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { CardHeader, CardBody, Tooltip } from '@patternfly/react-core';
|
import { CardHeader, Tooltip } from '@patternfly/react-core';
|
||||||
import { object } from 'prop-types';
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import { InventoriesAPI, CredentialTypesAPI } from '@api';
|
import { InventoriesAPI, CredentialTypesAPI } from '@api';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
@@ -58,7 +59,9 @@ function InventoryEdit({ history, i18n, inventory }) {
|
|||||||
} = values;
|
} = values;
|
||||||
try {
|
try {
|
||||||
await InventoriesAPI.update(inventory.id, {
|
await InventoriesAPI.update(inventory.id, {
|
||||||
insights_credential: insights_credential.id,
|
insights_credential: insights_credential
|
||||||
|
? insights_credential.id
|
||||||
|
: null,
|
||||||
organization: organization.id,
|
organization: organization.id,
|
||||||
...remainingValues,
|
...remainingValues,
|
||||||
});
|
});
|
||||||
@@ -76,13 +79,13 @@ function InventoryEdit({ history, i18n, inventory }) {
|
|||||||
);
|
);
|
||||||
await Promise.all([...associatePromises, ...disassociatePromises]);
|
await Promise.all([...associatePromises, ...disassociatePromises]);
|
||||||
}
|
}
|
||||||
|
const url =
|
||||||
|
history.location.pathname.search('smart') > -1
|
||||||
|
? `/inventories/smart_inventory/${inventory.id}/details`
|
||||||
|
: `/inventories/inventory/${inventory.id}/details`;
|
||||||
|
history.push(`${url}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err);
|
setError(err);
|
||||||
} finally {
|
|
||||||
const url = history.location.pathname.search('smart')
|
|
||||||
? `/inventories/smart_inventory/${inventory.id}/details`
|
|
||||||
: `/inventories/inventory/${inventory.id}/details`;
|
|
||||||
history.push(`${url}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (contentLoading) {
|
if (contentLoading) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { CardHeader } from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom';
|
import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom';
|
||||||
import { GroupsAPI } from '@api';
|
import { GroupsAPI } from '@api';
|
||||||
@@ -9,6 +8,7 @@ import CardCloseButton from '@components/CardCloseButton';
|
|||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
|
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
|
||||||
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
|
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
|
||||||
|
|
||||||
@@ -97,12 +97,12 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
|
|||||||
!history.location.pathname.endsWith('edit')
|
!history.location.pathname.endsWith('edit')
|
||||||
) {
|
) {
|
||||||
cardHeader = (
|
cardHeader = (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||||
<CardCloseButton
|
<CardCloseButton
|
||||||
linkTo={`/inventories/inventory/${inventory.id}/groups`}
|
linkTo={`/inventories/inventory/${inventory.id}/groups`}
|
||||||
/>
|
/>
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { CardBody, Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { withRouter, Link } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { VariablesInput as CodeMirrorInput } from '@components/CodeMirrorInput';
|
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import ErrorDetail from '@components/ErrorDetail';
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import { formatDateString } from '@util/dates';
|
|
||||||
|
|
||||||
import { GroupsAPI } from '@api';
|
import { GroupsAPI } from '@api';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||||
|
|
||||||
const VariablesInput = styled(CodeMirrorInput)`
|
// TODO: extract this into a component for use in all relevant Detail views
|
||||||
.pf-c-form__label {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
margin: 20px 0;
|
|
||||||
`;
|
|
||||||
const ActionButtonWrapper = styled.div`
|
const ActionButtonWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -28,6 +22,7 @@ const ActionButtonWrapper = styled.div`
|
|||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
|
function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
|
||||||
const {
|
const {
|
||||||
summary_fields: { created_by, modified_by },
|
summary_fields: { created_by, modified_by },
|
||||||
@@ -50,52 +45,26 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let createdBy = '';
|
|
||||||
if (created) {
|
|
||||||
if (created_by && created_by.username) {
|
|
||||||
createdBy = (
|
|
||||||
<span>
|
|
||||||
{i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '}
|
|
||||||
<Link to={`/users/${created_by.id}`}>{created_by.username}</Link>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
createdBy = formatDateString(inventoryGroup.created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let modifiedBy = '';
|
|
||||||
if (modified) {
|
|
||||||
if (modified_by && modified_by.username) {
|
|
||||||
modifiedBy = (
|
|
||||||
<span>
|
|
||||||
{i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '}
|
|
||||||
<Link to={`/users/${modified_by.id}`}>{modified_by.username}</Link>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
modifiedBy = formatDateString(inventoryGroup.modified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody css="padding-top: 20px">
|
<CardBody>
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
<Detail label={i18n._(t`Name`)} value={name} />
|
<Detail label={i18n._(t`Name`)} value={name} />
|
||||||
<Detail label={i18n._(t`Description`)} value={description} />
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
</DetailList>
|
<VariablesDetail
|
||||||
<VariablesInput
|
label={i18n._(t`Variables`)}
|
||||||
id="inventoryGroup-variables"
|
value={variables}
|
||||||
readOnly
|
rows={4}
|
||||||
value={variables}
|
/>
|
||||||
rows={4}
|
<UserDateDetail
|
||||||
label={i18n._(t`Variables`)}
|
label={i18n._(t`Created`)}
|
||||||
/>
|
date={created}
|
||||||
<DetailList>
|
user={created_by}
|
||||||
{createdBy && <Detail label={i18n._(t`Created`)} value={createdBy} />}
|
/>
|
||||||
{modifiedBy && (
|
<UserDateDetail
|
||||||
<Detail label={i18n._(t`Modified`)} value={modifiedBy} />
|
label={i18n._(t`Last Modified`)}
|
||||||
)}
|
date={modified}
|
||||||
|
user={modified_by}
|
||||||
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<ActionButtonWrapper>
|
<ActionButtonWrapper>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ describe('<InventoryGroupDetail />', () => {
|
|||||||
'Bar'
|
'Bar'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('Detail[label="Created"]').length).toBe(1);
|
expect(wrapper.find('Detail[label="Created"]').length).toBe(1);
|
||||||
expect(wrapper.find('Detail[label="Modified"]').length).toBe(1);
|
expect(wrapper.find('Detail[label="Last Modified"]').length).toBe(1);
|
||||||
expect(wrapper.find('VariablesInput').prop('value')).toBe('bizz: buzz');
|
expect(wrapper.find('VariablesDetail').prop('value')).toBe('bizz: buzz');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
import InventoryHostForm from '../shared/InventoryHostForm';
|
import InventoryHostForm from '../shared/InventoryHostForm';
|
||||||
import { InventoriesAPI } from '@api';
|
import { InventoriesAPI } from '@api';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class InventorySources extends Component {
|
class InventorySources extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
||||||
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
@@ -74,10 +75,10 @@ class SmartInventory extends Component {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let cardHeader = hasContentLoading ? null : (
|
let cardHeader = hasContentLoading ? null : (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||||
<CardCloseButton linkTo="/inventories" />
|
<CardCloseButton linkTo="/inventories" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (location.pathname.endsWith('edit')) {
|
if (location.pathname.endsWith('edit')) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class SmartInventoryCompletedJobs extends Component {
|
class SmartInventoryCompletedJobs extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class SmartInventoryDetail extends Component {
|
class SmartInventoryDetail extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class SmartInventoryHosts extends Component {
|
class SmartInventoryHosts extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import React from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { Form, Card, CardBody } from '@patternfly/react-core';
|
import { Form, Card } from '@patternfly/react-core';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import FormRow from '@components/FormRow';
|
import FormRow from '@components/FormRow';
|
||||||
import FormField from '@components/FormField';
|
import FormField from '@components/FormField';
|
||||||
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
||||||
|
|||||||
@@ -2,13 +2,9 @@ import React, { Component } from 'react';
|
|||||||
import { Route, withRouter, Switch, Redirect, Link } from 'react-router-dom';
|
import { Route, withRouter, Switch, Redirect, Link } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardHeader as PFCardHeader,
|
|
||||||
PageSection,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import { JobsAPI } from '@api';
|
import { JobsAPI } from '@api';
|
||||||
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
@@ -17,13 +13,6 @@ import JobDetail from './JobDetail';
|
|||||||
import JobOutput from './JobOutput';
|
import JobOutput from './JobOutput';
|
||||||
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
|
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
|
||||||
|
|
||||||
const CardHeader = styled(PFCardHeader)`
|
|
||||||
--pf-c-card--first-child--PaddingTop: 0;
|
|
||||||
--pf-c-card--child--PaddingLeft: 0;
|
|
||||||
--pf-c-card--child--PaddingRight: 0;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class Job extends Component {
|
class Job extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -81,10 +70,10 @@ class Job extends Component {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
<CardHeader>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs match={match} history={history} tabsArray={tabsArray} />
|
<RoutedTabs match={match} history={history} tabsArray={tabsArray} />
|
||||||
<CardCloseButton linkTo="/jobs" />
|
<CardCloseButton linkTo="/jobs" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import React, { useState } from 'react';
|
|||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, 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 { CardBody, Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
import { DetailList, Detail } from '@components/DetailList';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
||||||
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
|
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
|
||||||
import ErrorDetail from '@components/ErrorDetail';
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from 'react-virtualized';
|
} from 'react-virtualized';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
import { JobsAPI } from '@api';
|
import { JobsAPI } from '@api';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
|
|||||||
@@ -2,13 +2,9 @@ import React, { Component } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||||
import {
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
Card,
|
|
||||||
CardHeader as PFCardHeader,
|
|
||||||
PageSection,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import NotificationList from '@components/NotificationList/NotificationList';
|
import NotificationList from '@components/NotificationList/NotificationList';
|
||||||
@@ -18,13 +14,6 @@ import OrganizationEdit from './OrganizationEdit';
|
|||||||
import OrganizationTeams from './OrganizationTeams';
|
import OrganizationTeams from './OrganizationTeams';
|
||||||
import { OrganizationsAPI } from '@api';
|
import { OrganizationsAPI } from '@api';
|
||||||
|
|
||||||
const CardHeader = styled(PFCardHeader)`
|
|
||||||
--pf-c-card--first-child--PaddingTop: 0;
|
|
||||||
--pf-c-card--child--PaddingLeft: 0;
|
|
||||||
--pf-c-card--child--PaddingRight: 0;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class Organization extends Component {
|
class Organization extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -141,7 +130,7 @@ class Organization extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs
|
<RoutedTabs
|
||||||
match={match}
|
match={match}
|
||||||
history={history}
|
history={history}
|
||||||
@@ -149,7 +138,7 @@ class Organization extends Component {
|
|||||||
tabsArray={tabsArray}
|
tabsArray={tabsArray}
|
||||||
/>
|
/>
|
||||||
<CardCloseButton linkTo="/organizations" />
|
<CardCloseButton linkTo="/organizations" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
|
|||||||
@@ -3,16 +3,11 @@ import PropTypes from 'prop-types';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } 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 { PageSection, Card, CardHeader, Tooltip } from '@patternfly/react-core';
|
||||||
PageSection,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { OrganizationsAPI } from '@api';
|
import { OrganizationsAPI } from '@api';
|
||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import OrganizationForm from '../shared/OrganizationForm';
|
import OrganizationForm from '../shared/OrganizationForm';
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,13 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Link, useRouteMatch } from 'react-router-dom';
|
import { Link, useRouteMatch } 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 { CardBody as PFCardBody, Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { OrganizationsAPI } from '@api';
|
import { OrganizationsAPI } from '@api';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import { ChipGroup, Chip } from '@components/Chip';
|
import { ChipGroup, Chip } from '@components/Chip';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import { formatDateString } from '@util/dates';
|
|
||||||
|
|
||||||
const CardBody = styled(PFCardBody)`
|
|
||||||
padding-top: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function OrganizationDetail({ i18n, organization }) {
|
function OrganizationDetail({ i18n, organization }) {
|
||||||
const {
|
const {
|
||||||
@@ -72,10 +66,15 @@ function OrganizationDetail({ i18n, organization }) {
|
|||||||
label={i18n._(t`Ansible Environment`)}
|
label={i18n._(t`Ansible Environment`)}
|
||||||
value={custom_virtualenv}
|
value={custom_virtualenv}
|
||||||
/>
|
/>
|
||||||
<Detail label={i18n._(t`Created`)} value={formatDateString(created)} />
|
<UserDateDetail
|
||||||
<Detail
|
label={i18n._(t`Created`)}
|
||||||
|
date={created}
|
||||||
|
user={summary_fields.created_by}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
label={i18n._(t`Last Modified`)}
|
label={i18n._(t`Last Modified`)}
|
||||||
value={formatDateString(modified)}
|
date={modified}
|
||||||
|
user={summary_fields.modified_by}
|
||||||
/>
|
/>
|
||||||
{instanceGroups && instanceGroups.length > 0 && (
|
{instanceGroups && instanceGroups.length > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
import { OrganizationsAPI } from '@api';
|
import { OrganizationsAPI } from '@api';
|
||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ import React, { Component } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||||
import {
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
Card,
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
CardHeader as PFCardHeader,
|
|
||||||
PageSection,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
@@ -19,13 +15,6 @@ import ProjectJobTemplates from './ProjectJobTemplates';
|
|||||||
import ProjectSchedules from './ProjectSchedules';
|
import ProjectSchedules from './ProjectSchedules';
|
||||||
import { OrganizationsAPI, ProjectsAPI } from '@api';
|
import { OrganizationsAPI, ProjectsAPI } from '@api';
|
||||||
|
|
||||||
const CardHeader = styled(PFCardHeader)`
|
|
||||||
--pf-c-card--first-child--PaddingTop: 0;
|
|
||||||
--pf-c-card--child--PaddingLeft: 0;
|
|
||||||
--pf-c-card--child--PaddingRight: 0;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class Project extends Component {
|
class Project extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -161,7 +150,7 @@ class Project extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs
|
<RoutedTabs
|
||||||
match={match}
|
match={match}
|
||||||
history={history}
|
history={history}
|
||||||
@@ -169,7 +158,7 @@ class Project extends Component {
|
|||||||
tabsArray={tabsArray}
|
tabsArray={tabsArray}
|
||||||
/>
|
/>
|
||||||
<CardCloseButton linkTo="/projects" />
|
<CardCloseButton linkTo="/projects" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { t } from '@lingui/macro';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import {
|
||||||
Card as _Card,
|
Card as _Card,
|
||||||
CardBody,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
PageSection,
|
PageSection,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import ProjectForm from '../shared/ProjectForm';
|
import ProjectForm from '../shared/ProjectForm';
|
||||||
import { ProjectsAPI } from '@api';
|
import { ProjectsAPI } from '@api';
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ function ProjectAdd({ history, i18n }) {
|
|||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader css="text-align: right">
|
<CardHeader className="at-u-textRight">
|
||||||
<Tooltip content={i18n._(t`Close`)} position="top">
|
<Tooltip content={i18n._(t`Close`)} position="top">
|
||||||
<CardCloseButton onClick={handleCancel} />
|
<CardCloseButton onClick={handleCancel} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Project } from '@types';
|
import { Project } from '@types';
|
||||||
import { formatDateString } from '@util/dates';
|
|
||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
import { Button, CardBody, List, ListItem } from '@patternfly/react-core';
|
import { Button, List, ListItem } from '@patternfly/react-core';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
import { CardBody } from '@components/Card';
|
||||||
|
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||||
import { CredentialChip } from '@components/Chip';
|
import { CredentialChip } from '@components/Chip';
|
||||||
import { toTitleCase } from '@util/strings';
|
import { toTitleCase } from '@util/strings';
|
||||||
|
|
||||||
@@ -64,32 +64,8 @@ function ProjectDetail({ project, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let createdBy = '';
|
|
||||||
if (created) {
|
|
||||||
if (summary_fields.created_by && summary_fields.created_by.username) {
|
|
||||||
createdBy = i18n._(
|
|
||||||
t`${formatDateString(created)} by ${summary_fields.created_by.username}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
createdBy = formatDateString(created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let modifiedBy = '';
|
|
||||||
if (modified) {
|
|
||||||
if (summary_fields.modified_by && summary_fields.modified_by.username) {
|
|
||||||
modifiedBy = i18n._(
|
|
||||||
t`${formatDateString(modified)} by ${
|
|
||||||
summary_fields.modified_by.username
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
modifiedBy = formatDateString(modified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody css="padding-top: 20px">
|
<CardBody>
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Name`)}
|
label={i18n._(t`Name`)}
|
||||||
@@ -150,10 +126,16 @@ function ProjectDetail({ project, i18n }) {
|
|||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
<Detail label={i18n._(t`Playbook Directory`)} value={local_path} />
|
<Detail label={i18n._(t`Playbook Directory`)} value={local_path} />
|
||||||
{/* TODO: Link to user in users */}
|
<UserDateDetail
|
||||||
<Detail label={i18n._(t`Created`)} value={createdBy} />
|
label={i18n._(t`Created`)}
|
||||||
{/* TODO: Link to user in users */}
|
date={created}
|
||||||
<Detail label={i18n._(t`Last Modified`)} value={modifiedBy} />
|
user={summary_fields.created_by}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Last Modified`)}
|
||||||
|
date={modified}
|
||||||
|
user={summary_fields.modified_by}
|
||||||
|
/>
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<ActionButtonWrapper>
|
<ActionButtonWrapper>
|
||||||
{summary_fields.user_capabilities &&
|
{summary_fields.user_capabilities &&
|
||||||
|
|||||||
@@ -98,13 +98,15 @@ describe('<ProjectDetail />', () => {
|
|||||||
`${mockProject.scm_update_cache_timeout} Seconds`
|
`${mockProject.scm_update_cache_timeout} Seconds`
|
||||||
);
|
);
|
||||||
assertDetail('Ansible Environment', mockProject.custom_virtualenv);
|
assertDetail('Ansible Environment', mockProject.custom_virtualenv);
|
||||||
assertDetail(
|
const dateDetails = wrapper.find('UserDateDetail');
|
||||||
'Created',
|
expect(dateDetails).toHaveLength(2);
|
||||||
`10/10/2019, 1:15:06 AM by ${mockProject.summary_fields.created_by.username}`
|
expect(dateDetails.at(0).prop('label')).toEqual('Created');
|
||||||
|
expect(dateDetails.at(0).prop('date')).toEqual(
|
||||||
|
'2019-10-10T01:15:06.780472Z'
|
||||||
);
|
);
|
||||||
assertDetail(
|
expect(dateDetails.at(1).prop('label')).toEqual('Last Modified');
|
||||||
'Last Modified',
|
expect(dateDetails.at(1).prop('date')).toEqual(
|
||||||
`10/10/2019, 1:15:06 AM by ${mockProject.summary_fields.modified_by.username}`
|
'2019-10-10T01:15:06.780490Z'
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ import { withRouter } from 'react-router-dom';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import { Card as _Card, CardHeader, Tooltip } from '@patternfly/react-core';
|
||||||
Card as _Card,
|
import { CardBody } from '@components/Card';
|
||||||
CardBody,
|
|
||||||
CardHeader,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import ProjectForm from '../shared/ProjectForm';
|
import ProjectForm from '../shared/ProjectForm';
|
||||||
import { ProjectsAPI } from '@api';
|
import { ProjectsAPI } from '@api';
|
||||||
|
|
||||||
|
// TODO: we are doing this in multiple add/edit screens -- move to
|
||||||
|
// common component?
|
||||||
const Card = styled(_Card)`
|
const Card = styled(_Card)`
|
||||||
--pf-c-card--child--PaddingLeft: 0;
|
--pf-c-card--child--PaddingLeft: 0;
|
||||||
--pf-c-card--child--PaddingRight: 0;
|
--pf-c-card--child--PaddingRight: 0;
|
||||||
@@ -41,7 +39,7 @@ function ProjectEdit({ project, history, i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader css="text-align: right">
|
<CardHeader className="at-u-textRight">
|
||||||
<Tooltip content={i18n._(t`Close`)} position="top">
|
<Tooltip content={i18n._(t`Close`)} position="top">
|
||||||
<CardCloseButton onClick={handleCancel} />
|
<CardCloseButton onClick={handleCancel} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class ProjectJobTemplates extends Component {
|
class ProjectJobTemplates extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class ProjectSchedules extends Component {
|
class ProjectSchedules extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -2,26 +2,15 @@ import React, { Component } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||||
import {
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
Card,
|
|
||||||
CardHeader as PFCardHeader,
|
|
||||||
PageSection,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import TeamDetail from './TeamDetail';
|
import TeamDetail from './TeamDetail';
|
||||||
import TeamEdit from './TeamEdit';
|
import TeamEdit from './TeamEdit';
|
||||||
import { TeamsAPI } from '@api';
|
import { TeamsAPI } from '@api';
|
||||||
|
|
||||||
const CardHeader = styled(PFCardHeader)`
|
|
||||||
--pf-c-card--first-child--PaddingTop: 0;
|
|
||||||
--pf-c-card--child--PaddingLeft: 0;
|
|
||||||
--pf-c-card--child--PaddingRight: 0;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class Team extends Component {
|
class Team extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -81,7 +70,7 @@ class Team extends Component {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs
|
<RoutedTabs
|
||||||
match={match}
|
match={match}
|
||||||
history={history}
|
history={history}
|
||||||
@@ -89,7 +78,7 @@ class Team extends Component {
|
|||||||
tabsArray={tabsArray}
|
tabsArray={tabsArray}
|
||||||
/>
|
/>
|
||||||
<CardCloseButton linkTo="/teams" />
|
<CardCloseButton linkTo="/teams" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
|
|||||||
@@ -2,16 +2,11 @@ import React from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import { PageSection, Card, CardHeader, Tooltip } from '@patternfly/react-core';
|
||||||
PageSection,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
import { TeamsAPI } from '@api';
|
import { TeamsAPI } from '@api';
|
||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
|
|
||||||
import TeamForm from '../shared/TeamForm';
|
import TeamForm from '../shared/TeamForm';
|
||||||
|
|||||||
@@ -2,16 +2,12 @@ import React, { Component } from 'react';
|
|||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, 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 { CardBody as PFCardBody, Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
import { DetailList, Detail } from '@components/DetailList';
|
||||||
import { formatDateString } from '@util/dates';
|
import { formatDateString } from '@util/dates';
|
||||||
|
|
||||||
const CardBody = styled(PFCardBody)`
|
|
||||||
padding-top: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class TeamDetail extends Component {
|
class TeamDetail extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
import { TeamsAPI } from '@api';
|
import { TeamsAPI } from '@api';
|
||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
|
|||||||
@@ -2,13 +2,8 @@ import React, { useState } from 'react';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import { Card, CardHeader, PageSection, Tooltip } from '@patternfly/react-core';
|
||||||
Card,
|
import { CardBody } from '@components/Card';
|
||||||
CardBody,
|
|
||||||
CardHeader,
|
|
||||||
PageSection,
|
|
||||||
Tooltip,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import JobTemplateForm from '../shared/JobTemplateForm';
|
import JobTemplateForm from '../shared/JobTemplateForm';
|
||||||
import { JobTemplatesAPI } from '@api';
|
import { JobTemplatesAPI } from '@api';
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import {
|
import {
|
||||||
CardBody,
|
|
||||||
Button,
|
Button,
|
||||||
TextList,
|
TextList,
|
||||||
TextListItem,
|
TextListItem,
|
||||||
@@ -12,12 +11,12 @@ import {
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import LaunchButton from '@components/LaunchButton';
|
import LaunchButton from '@components/LaunchButton';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||||
import { formatDateString } from '@util/dates';
|
|
||||||
import { JobTemplatesAPI } from '@api';
|
import { JobTemplatesAPI } from '@api';
|
||||||
|
|
||||||
const ButtonGroup = styled.div`
|
const ButtonGroup = styled.div`
|
||||||
@@ -112,32 +111,6 @@ class JobTemplateDetail extends Component {
|
|||||||
const renderOptionsField =
|
const renderOptionsField =
|
||||||
become_enabled || host_config_key || allow_simultaneous || use_fact_cache;
|
become_enabled || host_config_key || allow_simultaneous || use_fact_cache;
|
||||||
|
|
||||||
let createdBy = '';
|
|
||||||
if (created) {
|
|
||||||
if (summary_fields.created_by && summary_fields.created_by.username) {
|
|
||||||
createdBy = i18n._(
|
|
||||||
t`${formatDateString(created)} by ${
|
|
||||||
summary_fields.created_by.username
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
createdBy = formatDateString(created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let modifiedBy = '';
|
|
||||||
if (modified) {
|
|
||||||
if (summary_fields.modified_by && summary_fields.modified_by.username) {
|
|
||||||
modifiedBy = i18n._(
|
|
||||||
t`${formatDateString(modified)} by ${
|
|
||||||
summary_fields.modified_by.username
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
modifiedBy = formatDateString(modified);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderOptions = (
|
const renderOptions = (
|
||||||
<TextList component={TextListVariants.ul}>
|
<TextList component={TextListVariants.ul}>
|
||||||
{become_enabled && (
|
{become_enabled && (
|
||||||
@@ -195,7 +168,7 @@ class JobTemplateDetail extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
isInitialized && (
|
isInitialized && (
|
||||||
<CardBody css="padding-top: 20px;">
|
<CardBody>
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Name`)}
|
label={i18n._(t`Name`)}
|
||||||
@@ -239,18 +212,16 @@ class JobTemplateDetail extends Component {
|
|||||||
value={verbosityDetails[0].details}
|
value={verbosityDetails[0].details}
|
||||||
/>
|
/>
|
||||||
<Detail label={i18n._(t`Timeout`)} value={timeout || '0'} />
|
<Detail label={i18n._(t`Timeout`)} value={timeout || '0'} />
|
||||||
{createdBy && (
|
<UserDateDetail
|
||||||
<Detail
|
label={i18n._(t`Created`)}
|
||||||
label={i18n._(t`Created`)}
|
date={created}
|
||||||
value={createdBy} // TODO: link to user in users
|
user={summary_fields.created_by}
|
||||||
/>
|
/>
|
||||||
)}
|
<UserDateDetail
|
||||||
{modifiedBy && (
|
label={i18n._(t`Last Modified`)}
|
||||||
<Detail
|
date={modified}
|
||||||
label={i18n._(t`Last Modified`)}
|
user={summary_fields.modified_by}
|
||||||
value={modifiedBy} // TODO: link to user in users
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Show Changes`)}
|
label={i18n._(t`Show Changes`)}
|
||||||
value={diff_mode ? 'On' : 'Off'}
|
value={diff_mode ? 'On' : 'Off'}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint react/no-unused-state: 0 */
|
/* eslint react/no-unused-state: 0 */
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { withRouter, Redirect } from 'react-router-dom';
|
import { withRouter, Redirect } from 'react-router-dom';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import { JobTemplatesAPI, ProjectsAPI } from '@api';
|
import { JobTemplatesAPI, ProjectsAPI } from '@api';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
||||||
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import NotificationList from '@components/NotificationList';
|
import NotificationList from '@components/NotificationList';
|
||||||
@@ -121,10 +122,10 @@ class Template extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||||
<CardCloseButton linkTo="/templates" />
|
<CardCloseButton linkTo="/templates" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (location.pathname.endsWith('edit')) {
|
if (location.pathname.endsWith('edit')) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
||||||
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
import AppendBody from '@components/AppendBody';
|
import AppendBody from '@components/AppendBody';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
@@ -65,10 +66,10 @@ class WorkflowJobTemplate extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cardHeader = hasContentLoading ? null : (
|
let cardHeader = hasContentLoading ? null : (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||||
<CardCloseButton linkTo="/templates" />
|
<CardCloseButton linkTo="/templates" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (location.pathname.endsWith('edit')) {
|
if (location.pathname.endsWith('edit')) {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
import { DetailList } from '@components/DetailList';
|
import { DetailList } from '@components/DetailList';
|
||||||
|
|
||||||
class WorkflowJobTemplateDetail extends Component {
|
class WorkflowJobTemplateDetail extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<CardBody css="padding-top: 20px;">
|
<CardBody>
|
||||||
<DetailList gutter="sm" />
|
<DetailList gutter="sm" />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ import React, { Component } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||||
import {
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
Card,
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
CardHeader as PFCardHeader,
|
|
||||||
PageSection,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import RoutedTabs from '@components/RoutedTabs';
|
import RoutedTabs from '@components/RoutedTabs';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
@@ -18,13 +14,6 @@ import UserTeams from './UserTeams';
|
|||||||
import UserTokens from './UserTokens';
|
import UserTokens from './UserTokens';
|
||||||
import { UsersAPI } from '@api';
|
import { UsersAPI } from '@api';
|
||||||
|
|
||||||
const CardHeader = styled(PFCardHeader)`
|
|
||||||
--pf-c-card--first-child--PaddingTop: 0;
|
|
||||||
--pf-c-card--child--PaddingLeft: 0;
|
|
||||||
--pf-c-card--child--PaddingRight: 0;
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class User extends Component {
|
class User extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -90,7 +79,7 @@ class User extends Component {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let cardHeader = (
|
let cardHeader = (
|
||||||
<CardHeader style={{ padding: 0 }}>
|
<TabbedCardHeader>
|
||||||
<RoutedTabs
|
<RoutedTabs
|
||||||
match={match}
|
match={match}
|
||||||
history={history}
|
history={history}
|
||||||
@@ -98,7 +87,7 @@ class User extends Component {
|
|||||||
tabsArray={tabsArray}
|
tabsArray={tabsArray}
|
||||||
/>
|
/>
|
||||||
<CardCloseButton linkTo="/users" />
|
<CardCloseButton linkTo="/users" />
|
||||||
</CardHeader>
|
</TabbedCardHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { t } from '@lingui/macro';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import {
|
||||||
Card as _Card,
|
Card as _Card,
|
||||||
CardBody,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
PageSection,
|
PageSection,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import UserForm from '../shared/UserForm';
|
import UserForm from '../shared/UserForm';
|
||||||
import { UsersAPI } from '@api';
|
import { UsersAPI } from '@api';
|
||||||
@@ -41,7 +41,7 @@ function UserAdd({ history, i18n }) {
|
|||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader css="text-align: right">
|
<CardHeader className="at-u-textRight">
|
||||||
<Tooltip content={i18n._(t`Close`)} position="top">
|
<Tooltip content={i18n._(t`Close`)} position="top">
|
||||||
<CardCloseButton onClick={handleCancel} />
|
<CardCloseButton onClick={handleCancel} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -2,16 +2,12 @@ import React, { Component } from 'react';
|
|||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, 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 { CardBody as PFCardBody, Button } from '@patternfly/react-core';
|
import { Button } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
|
import { CardBody } from '@components/Card';
|
||||||
import { DetailList, Detail } from '@components/DetailList';
|
import { DetailList, Detail } from '@components/DetailList';
|
||||||
import { formatDateString } from '@util/dates';
|
import { formatDateString } from '@util/dates';
|
||||||
|
|
||||||
const CardBody = styled(PFCardBody)`
|
|
||||||
padding-top: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class UserDetail extends Component {
|
class UserDetail extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
import UserForm from '../shared/UserForm';
|
import UserForm from '../shared/UserForm';
|
||||||
import { UsersAPI } from '@api';
|
import { UsersAPI } from '@api';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class UserAdd extends Component {
|
class UserAdd extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class UserAdd extends Component {
|
class UserAdd extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@components/Card';
|
||||||
|
|
||||||
class UserAdd extends Component {
|
class UserAdd extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -228,6 +228,14 @@ export const User = shape({
|
|||||||
last_login: string,
|
last_login: string,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// stripped-down User object found in summary_fields (e.g. modified_by)
|
||||||
|
export const SummaryFieldUser = shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
username: string.isRequired,
|
||||||
|
first_name: string,
|
||||||
|
last_name: string,
|
||||||
|
});
|
||||||
|
|
||||||
export const Group = shape({
|
export const Group = shape({
|
||||||
id: number.isRequired,
|
id: number.isRequired,
|
||||||
type: oneOf(['group']),
|
type: oneOf(['group']),
|
||||||
|
|||||||
Reference in New Issue
Block a user