mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -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:
commit
df5aa8a47d
@ -112,7 +112,7 @@ afterEach(() => {
|
||||
...
|
||||
```
|
||||
|
||||
**Test Attributes** -
|
||||
**Test Attributes** -
|
||||
It should be noted that the `dataCy` prop, as well as its equivalent attribute `data-cy`, are used as flags for any UI test that wants to avoid relying on brittle CSS selectors such as `nth-of-type()`.
|
||||
|
||||
## Handling API Errors
|
||||
@ -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:** 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).
|
||||
|
||||
|
||||
@ -156,13 +156,6 @@
|
||||
// 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 {
|
||||
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 = {
|
||||
value: string.isRequired,
|
||||
onChange: func.isRequired,
|
||||
onChange: func,
|
||||
mode: oneOf(['javascript', 'yaml', 'jinja2']).isRequired,
|
||||
readOnly: bool,
|
||||
hasErrors: bool,
|
||||
@ -91,6 +91,7 @@ CodeMirrorInput.propTypes = {
|
||||
};
|
||||
CodeMirrorInput.defaultProps = {
|
||||
readOnly: false,
|
||||
onChange: () => {},
|
||||
rows: 6,
|
||||
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 { string, bool } from 'prop-types';
|
||||
import { Field } from 'formik';
|
||||
import { Button, Split, SplitItem } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
import { Split, SplitItem } from '@patternfly/react-core';
|
||||
import CodeMirrorInput from './CodeMirrorInput';
|
||||
import YamlJsonToggle from './YamlJsonToggle';
|
||||
import { yamlToJson, jsonToYaml } from '../../util/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 }) {
|
||||
// TODO: detect initial mode
|
||||
const [mode, setMode] = useState(YAML_MODE);
|
||||
|
||||
return (
|
||||
@ -30,40 +24,21 @@ function VariablesField({ id, name, label, readOnly }) {
|
||||
</label>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<ButtonGroup>
|
||||
<SmallButton
|
||||
onClick={() => {
|
||||
if (mode === YAML_MODE) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
form.setFieldValue(name, jsonToYaml(field.value));
|
||||
setMode(YAML_MODE);
|
||||
} 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>
|
||||
<YamlJsonToggle
|
||||
mode={mode}
|
||||
onChange={newMode => {
|
||||
try {
|
||||
const newVal =
|
||||
newMode === YAML_MODE
|
||||
? jsonToYaml(field.value)
|
||||
: yamlToJson(field.value);
|
||||
form.setFieldValue(name, newVal);
|
||||
setMode(newMode);
|
||||
} catch (err) {
|
||||
form.setFieldError(name, err.message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
<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';
|
||||
|
||||
export default CodeMirrorInput;
|
||||
export { default as VariablesDetail } from './VariablesDetail';
|
||||
export { default as VariablesInput } from './VariablesInput';
|
||||
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 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 { t } from '@lingui/macro';
|
||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
CardHeader as PFCardHeader,
|
||||
PageSection,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
@ -18,13 +14,6 @@ import HostGroups from './HostGroups';
|
||||
import HostCompletedJobs from './HostCompletedJobs';
|
||||
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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -89,7 +78,7 @@ class Host extends Component {
|
||||
];
|
||||
|
||||
let cardHeader = (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs
|
||||
match={match}
|
||||
history={history}
|
||||
@ -97,7 +86,7 @@ class Host extends Component {
|
||||
tabsArray={tabsArray}
|
||||
/>
|
||||
<CardCloseButton linkTo="/hosts" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (!isInitialized) {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
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 { Config } from '@contexts/Config';
|
||||
import HostForm from '../shared';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class HostCompletedJobs extends Component {
|
||||
render() {
|
||||
|
||||
@ -4,10 +4,10 @@ import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { Host } from '@types';
|
||||
import { formatDateString } from '@util/dates';
|
||||
import { Button, CardBody } from '@patternfly/react-core';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import CodeMirrorInput from '@components/CodeMirrorInput';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||
import { VariablesDetail } from '@components/CodeMirrorInput';
|
||||
|
||||
const ActionButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
@ -21,32 +21,8 @@ const ActionButtonWrapper = styled.div`
|
||||
function HostDetail({ host, i18n }) {
|
||||
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 (
|
||||
<CardBody css="padding-top: 20px">
|
||||
<CardBody>
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
@ -66,23 +42,20 @@ function HostDetail({ host, i18n }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* TODO: Link to user in users */}
|
||||
<Detail label={i18n._(t`Created`)} value={createdBy} />
|
||||
{/* TODO: Link to user in users */}
|
||||
<Detail label={i18n._(t`Last Modified`)} value={modifiedBy} />
|
||||
<Detail
|
||||
fullWidth
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
user={summary_fields.created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Last Modified`)}
|
||||
date={modified}
|
||||
user={summary_fields.modified_by}
|
||||
/>
|
||||
<VariablesDetail
|
||||
label={i18n._(t`Variables`)}
|
||||
value={
|
||||
<CodeMirrorInput
|
||||
mode="yaml"
|
||||
readOnly
|
||||
value={host.variables}
|
||||
onChange={() => {}}
|
||||
rows={6}
|
||||
hasErrors={false}
|
||||
/>
|
||||
}
|
||||
value={host.variables}
|
||||
rows={6}
|
||||
/>
|
||||
</DetailList>
|
||||
<ActionButtonWrapper>
|
||||
|
||||
@ -30,7 +30,7 @@ describe('<HostDetail />', () => {
|
||||
mountWithContexts(<HostDetail host={mockHost} />);
|
||||
});
|
||||
|
||||
test('should render Details', async done => {
|
||||
test('should render Details', async () => {
|
||||
const wrapper = mountWithContexts(<HostDetail host={mockHost} />);
|
||||
const testParams = [
|
||||
{ label: 'Name', value: 'Foo' },
|
||||
@ -46,23 +46,22 @@ describe('<HostDetail />', () => {
|
||||
expect(detail.find('dt').text()).toBe(label);
|
||||
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 editButton = await waitForElement(wrapper, 'HostDetail Button');
|
||||
// VariablesDetail has two buttons
|
||||
const editButton = wrapper.find('Button').at(2);
|
||||
expect(editButton.text()).toEqual('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 };
|
||||
readOnlyHost.summary_fields.user_capabilities.edit = false;
|
||||
const wrapper = mountWithContexts(<HostDetail host={readOnlyHost} />);
|
||||
await waitForElement(wrapper, 'HostDetail');
|
||||
expect(wrapper.find('HostDetail Button').length).toBe(0);
|
||||
done();
|
||||
// VariablesDetail has two buttons
|
||||
expect(wrapper.find('Button').length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
import { HostsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class HostFacts extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class HostGroups extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
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 { TabbedCardHeader } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import ContentError from '@components/ContentError';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
@ -51,10 +52,10 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||
];
|
||||
|
||||
let cardHeader = hasContentLoading ? null : (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||
<CardCloseButton linkTo="/inventories" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@ -2,14 +2,8 @@ import React, { useState, useEffect } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
PageSection,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { PageSection, Card, CardHeader, Tooltip } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class InventoryCompletedJobs extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,10 +1,124 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 {
|
||||
render() {
|
||||
return <CardBody>Coming soon :)</CardBody>;
|
||||
function InventoryDetail({ inventory, i18n }) {
|
||||
const [instanceGroups, setInstanceGroups] = useState([]);
|
||||
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 { withRouter } from 'react-router-dom';
|
||||
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 { CardBody } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import { InventoriesAPI, CredentialTypesAPI } from '@api';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
@ -58,7 +59,9 @@ function InventoryEdit({ history, i18n, inventory }) {
|
||||
} = values;
|
||||
try {
|
||||
await InventoriesAPI.update(inventory.id, {
|
||||
insights_credential: insights_credential.id,
|
||||
insights_credential: insights_credential
|
||||
? insights_credential.id
|
||||
: null,
|
||||
organization: organization.id,
|
||||
...remainingValues,
|
||||
});
|
||||
@ -76,13 +79,13 @@ function InventoryEdit({ history, i18n, inventory }) {
|
||||
);
|
||||
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) {
|
||||
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) {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { CardHeader } from '@patternfly/react-core';
|
||||
|
||||
import { Switch, Route, withRouter, Link, Redirect } from 'react-router-dom';
|
||||
import { GroupsAPI } from '@api';
|
||||
@ -9,6 +8,7 @@ import CardCloseButton from '@components/CardCloseButton';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import InventoryGroupEdit from '../InventoryGroupEdit/InventoryGroupEdit';
|
||||
import InventoryGroupDetail from '../InventoryGroupDetail/InventoryGroupDetail';
|
||||
|
||||
@ -97,12 +97,12 @@ function InventoryGroups({ i18n, match, setBreadcrumb, inventory, history }) {
|
||||
!history.location.pathname.endsWith('edit')
|
||||
) {
|
||||
cardHeader = (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||
<CardCloseButton
|
||||
linkTo={`/inventories/inventory/${inventory.id}/groups`}
|
||||
/>
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { CardBody, Button } from '@patternfly/react-core';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { withRouter, Link } from 'react-router-dom';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
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 AlertModal from '@components/AlertModal';
|
||||
import { formatDateString } from '@util/dates';
|
||||
|
||||
import { GroupsAPI } from '@api';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||
|
||||
const VariablesInput = styled(CodeMirrorInput)`
|
||||
.pf-c-form__label {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
margin: 20px 0;
|
||||
`;
|
||||
// TODO: extract this into a component for use in all relevant Detail views
|
||||
const ActionButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -28,6 +22,7 @@ const ActionButtonWrapper = styled.div`
|
||||
margin-left: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) {
|
||||
const {
|
||||
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 (
|
||||
<CardBody css="padding-top: 20px">
|
||||
<CardBody>
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
</DetailList>
|
||||
<VariablesInput
|
||||
id="inventoryGroup-variables"
|
||||
readOnly
|
||||
value={variables}
|
||||
rows={4}
|
||||
label={i18n._(t`Variables`)}
|
||||
/>
|
||||
<DetailList>
|
||||
{createdBy && <Detail label={i18n._(t`Created`)} value={createdBy} />}
|
||||
{modifiedBy && (
|
||||
<Detail label={i18n._(t`Modified`)} value={modifiedBy} />
|
||||
)}
|
||||
<VariablesDetail
|
||||
label={i18n._(t`Variables`)}
|
||||
value={variables}
|
||||
rows={4}
|
||||
/>
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
user={created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Last Modified`)}
|
||||
date={modified}
|
||||
user={modified_by}
|
||||
/>
|
||||
</DetailList>
|
||||
<ActionButtonWrapper>
|
||||
<Button
|
||||
|
||||
@ -80,7 +80,7 @@ describe('<InventoryGroupDetail />', () => {
|
||||
'Bar'
|
||||
);
|
||||
expect(wrapper.find('Detail[label="Created"]').length).toBe(1);
|
||||
expect(wrapper.find('Detail[label="Modified"]').length).toBe(1);
|
||||
expect(wrapper.find('VariablesInput').prop('value')).toBe('bizz: buzz');
|
||||
expect(wrapper.find('Detail[label="Last Modified"]').length).toBe(1);
|
||||
expect(wrapper.find('VariablesDetail').prop('value')).toBe('bizz: buzz');
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import InventoryHostForm from '../shared/InventoryHostForm';
|
||||
import { InventoriesAPI } from '@api';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class InventorySources extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
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 { TabbedCardHeader } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import ContentError from '@components/ContentError';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
@ -74,10 +75,10 @@ class SmartInventory extends Component {
|
||||
];
|
||||
|
||||
let cardHeader = hasContentLoading ? null : (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||
<CardCloseButton linkTo="/inventories" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class SmartInventoryCompletedJobs extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class SmartInventoryDetail extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class SmartInventoryHosts extends Component {
|
||||
render() {
|
||||
|
||||
@ -2,9 +2,10 @@ import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
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 { CardBody } from '@components/Card';
|
||||
import FormRow from '@components/FormRow';
|
||||
import FormField from '@components/FormField';
|
||||
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 { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Card,
|
||||
CardHeader as PFCardHeader,
|
||||
PageSection,
|
||||
} from '@patternfly/react-core';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { JobsAPI } from '@api';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import ContentError from '@components/ContentError';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
@ -17,13 +13,6 @@ import JobDetail from './JobDetail';
|
||||
import JobOutput from './JobOutput';
|
||||
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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -81,10 +70,10 @@ class Job extends Component {
|
||||
];
|
||||
|
||||
let cardHeader = (
|
||||
<CardHeader>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs match={match} history={history} tabsArray={tabsArray} />
|
||||
<CardCloseButton linkTo="/jobs" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (!isInitialized) {
|
||||
|
||||
@ -2,11 +2,12 @@ import React, { useState } from 'react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { CardBody, Button } from '@patternfly/react-core';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { CardBody } from '@components/Card';
|
||||
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
||||
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
} from 'react-virtualized';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
import { JobsAPI } from '@api';
|
||||
import ContentError from '@components/ContentError';
|
||||
|
||||
@ -2,13 +2,9 @@ import React, { Component } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
CardHeader as PFCardHeader,
|
||||
PageSection,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
import NotificationList from '@components/NotificationList/NotificationList';
|
||||
@ -18,13 +14,6 @@ import OrganizationEdit from './OrganizationEdit';
|
||||
import OrganizationTeams from './OrganizationTeams';
|
||||
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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -141,7 +130,7 @@ class Organization extends Component {
|
||||
}
|
||||
|
||||
let cardHeader = (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs
|
||||
match={match}
|
||||
history={history}
|
||||
@ -149,7 +138,7 @@ class Organization extends Component {
|
||||
tabsArray={tabsArray}
|
||||
/>
|
||||
<CardCloseButton linkTo="/organizations" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (!isInitialized) {
|
||||
|
||||
@ -3,16 +3,11 @@ import PropTypes from 'prop-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
PageSection,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { PageSection, Card, CardHeader, Tooltip } from '@patternfly/react-core';
|
||||
|
||||
import { OrganizationsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
import { CardBody } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import OrganizationForm from '../shared/OrganizationForm';
|
||||
|
||||
|
||||
@ -2,19 +2,13 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Link, useRouteMatch } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { CardBody as PFCardBody, Button } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Button } from '@patternfly/react-core';
|
||||
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 ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { formatDateString } from '@util/dates';
|
||||
|
||||
const CardBody = styled(PFCardBody)`
|
||||
padding-top: 20px;
|
||||
`;
|
||||
|
||||
function OrganizationDetail({ i18n, organization }) {
|
||||
const {
|
||||
@ -72,10 +66,15 @@ function OrganizationDetail({ i18n, organization }) {
|
||||
label={i18n._(t`Ansible Environment`)}
|
||||
value={custom_virtualenv}
|
||||
/>
|
||||
<Detail label={i18n._(t`Created`)} value={formatDateString(created)} />
|
||||
<Detail
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
user={summary_fields.created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Last Modified`)}
|
||||
value={formatDateString(modified)}
|
||||
date={modified}
|
||||
user={summary_fields.modified_by}
|
||||
/>
|
||||
{instanceGroups && instanceGroups.length > 0 && (
|
||||
<Detail
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
|
||||
import { CardBody } from '@components/Card';
|
||||
import { OrganizationsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
|
||||
|
||||
@ -2,12 +2,8 @@ import React, { Component } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
CardHeader as PFCardHeader,
|
||||
PageSection,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
@ -19,13 +15,6 @@ import ProjectJobTemplates from './ProjectJobTemplates';
|
||||
import ProjectSchedules from './ProjectSchedules';
|
||||
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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -161,7 +150,7 @@ class Project extends Component {
|
||||
});
|
||||
|
||||
let cardHeader = (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs
|
||||
match={match}
|
||||
history={history}
|
||||
@ -169,7 +158,7 @@ class Project extends Component {
|
||||
tabsArray={tabsArray}
|
||||
/>
|
||||
<CardCloseButton linkTo="/projects" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (!isInitialized) {
|
||||
|
||||
@ -5,12 +5,12 @@ import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Card as _Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
PageSection,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import { CardBody } from '@components/Card';
|
||||
import ProjectForm from '../shared/ProjectForm';
|
||||
import { ProjectsAPI } from '@api';
|
||||
|
||||
@ -44,7 +44,7 @@ function ProjectAdd({ history, i18n }) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardHeader css="text-align: right">
|
||||
<CardHeader className="at-u-textRight">
|
||||
<Tooltip content={i18n._(t`Close`)} position="top">
|
||||
<CardCloseButton onClick={handleCancel} />
|
||||
</Tooltip>
|
||||
|
||||
@ -4,10 +4,10 @@ import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import { Project } from '@types';
|
||||
import { formatDateString } from '@util/dates';
|
||||
import { Config } from '@contexts/Config';
|
||||
import { Button, CardBody, List, ListItem } from '@patternfly/react-core';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { Button, List, ListItem } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||
import { CredentialChip } from '@components/Chip';
|
||||
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 (
|
||||
<CardBody css="padding-top: 20px">
|
||||
<CardBody>
|
||||
<DetailList gutter="sm">
|
||||
<Detail
|
||||
label={i18n._(t`Name`)}
|
||||
@ -150,10 +126,16 @@ function ProjectDetail({ project, i18n }) {
|
||||
)}
|
||||
</Config>
|
||||
<Detail label={i18n._(t`Playbook Directory`)} value={local_path} />
|
||||
{/* TODO: Link to user in users */}
|
||||
<Detail label={i18n._(t`Created`)} value={createdBy} />
|
||||
{/* TODO: Link to user in users */}
|
||||
<Detail label={i18n._(t`Last Modified`)} value={modifiedBy} />
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
user={summary_fields.created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Last Modified`)}
|
||||
date={modified}
|
||||
user={summary_fields.modified_by}
|
||||
/>
|
||||
</DetailList>
|
||||
<ActionButtonWrapper>
|
||||
{summary_fields.user_capabilities &&
|
||||
|
||||
@ -98,13 +98,15 @@ describe('<ProjectDetail />', () => {
|
||||
`${mockProject.scm_update_cache_timeout} Seconds`
|
||||
);
|
||||
assertDetail('Ansible Environment', mockProject.custom_virtualenv);
|
||||
assertDetail(
|
||||
'Created',
|
||||
`10/10/2019, 1:15:06 AM by ${mockProject.summary_fields.created_by.username}`
|
||||
const dateDetails = wrapper.find('UserDateDetail');
|
||||
expect(dateDetails).toHaveLength(2);
|
||||
expect(dateDetails.at(0).prop('label')).toEqual('Created');
|
||||
expect(dateDetails.at(0).prop('date')).toEqual(
|
||||
'2019-10-10T01:15:06.780472Z'
|
||||
);
|
||||
assertDetail(
|
||||
'Last Modified',
|
||||
`10/10/2019, 1:15:06 AM by ${mockProject.summary_fields.modified_by.username}`
|
||||
expect(dateDetails.at(1).prop('label')).toEqual('Last Modified');
|
||||
expect(dateDetails.at(1).prop('date')).toEqual(
|
||||
'2019-10-10T01:15:06.780490Z'
|
||||
);
|
||||
expect(
|
||||
wrapper
|
||||
|
||||
@ -3,16 +3,14 @@ import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Card as _Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { Card as _Card, CardHeader, Tooltip } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import ProjectForm from '../shared/ProjectForm';
|
||||
import { ProjectsAPI } from '@api';
|
||||
|
||||
// TODO: we are doing this in multiple add/edit screens -- move to
|
||||
// common component?
|
||||
const Card = styled(_Card)`
|
||||
--pf-c-card--child--PaddingLeft: 0;
|
||||
--pf-c-card--child--PaddingRight: 0;
|
||||
@ -41,7 +39,7 @@ function ProjectEdit({ project, history, i18n }) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader css="text-align: right">
|
||||
<CardHeader className="at-u-textRight">
|
||||
<Tooltip content={i18n._(t`Close`)} position="top">
|
||||
<CardCloseButton onClick={handleCancel} />
|
||||
</Tooltip>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class ProjectJobTemplates extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class ProjectSchedules extends Component {
|
||||
render() {
|
||||
|
||||
@ -2,26 +2,15 @@ import React, { Component } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
CardHeader as PFCardHeader,
|
||||
PageSection,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
import TeamDetail from './TeamDetail';
|
||||
import TeamEdit from './TeamEdit';
|
||||
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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -81,7 +70,7 @@ class Team extends Component {
|
||||
];
|
||||
|
||||
let cardHeader = (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs
|
||||
match={match}
|
||||
history={history}
|
||||
@ -89,7 +78,7 @@ class Team extends Component {
|
||||
tabsArray={tabsArray}
|
||||
/>
|
||||
<CardCloseButton linkTo="/teams" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (!isInitialized) {
|
||||
|
||||
@ -2,16 +2,11 @@ import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
PageSection,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { PageSection, Card, CardHeader, Tooltip } from '@patternfly/react-core';
|
||||
|
||||
import { TeamsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
import { CardBody } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
|
||||
import TeamForm from '../shared/TeamForm';
|
||||
|
||||
@ -2,16 +2,12 @@ import React, { Component } from 'react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { CardBody as PFCardBody, Button } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
|
||||
import { CardBody } from '@components/Card';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { formatDateString } from '@util/dates';
|
||||
|
||||
const CardBody = styled(PFCardBody)`
|
||||
padding-top: 20px;
|
||||
`;
|
||||
|
||||
class TeamDetail extends Component {
|
||||
render() {
|
||||
const {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
import { TeamsAPI } from '@api';
|
||||
import { Config } from '@contexts/Config';
|
||||
|
||||
@ -2,13 +2,8 @@ import React, { useState } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
PageSection,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { Card, CardHeader, PageSection, Tooltip } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import JobTemplateForm from '../shared/JobTemplateForm';
|
||||
import { JobTemplatesAPI } from '@api';
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { Component, Fragment } from 'react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import {
|
||||
CardBody,
|
||||
Button,
|
||||
TextList,
|
||||
TextListItem,
|
||||
@ -12,12 +11,12 @@ import {
|
||||
import styled from 'styled-components';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { CardBody } from '@components/Card';
|
||||
import ContentError from '@components/ContentError';
|
||||
import LaunchButton from '@components/LaunchButton';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { formatDateString } from '@util/dates';
|
||||
import { DetailList, Detail, UserDateDetail } from '@components/DetailList';
|
||||
import { JobTemplatesAPI } from '@api';
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
@ -112,32 +111,6 @@ class JobTemplateDetail extends Component {
|
||||
const renderOptionsField =
|
||||
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 = (
|
||||
<TextList component={TextListVariants.ul}>
|
||||
{become_enabled && (
|
||||
@ -195,7 +168,7 @@ class JobTemplateDetail extends Component {
|
||||
|
||||
return (
|
||||
isInitialized && (
|
||||
<CardBody css="padding-top: 20px;">
|
||||
<CardBody>
|
||||
<DetailList gutter="sm">
|
||||
<Detail
|
||||
label={i18n._(t`Name`)}
|
||||
@ -239,18 +212,16 @@ class JobTemplateDetail extends Component {
|
||||
value={verbosityDetails[0].details}
|
||||
/>
|
||||
<Detail label={i18n._(t`Timeout`)} value={timeout || '0'} />
|
||||
{createdBy && (
|
||||
<Detail
|
||||
label={i18n._(t`Created`)}
|
||||
value={createdBy} // TODO: link to user in users
|
||||
/>
|
||||
)}
|
||||
{modifiedBy && (
|
||||
<Detail
|
||||
label={i18n._(t`Last Modified`)}
|
||||
value={modifiedBy} // TODO: link to user in users
|
||||
/>
|
||||
)}
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
user={summary_fields.created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Last Modified`)}
|
||||
date={modified}
|
||||
user={summary_fields.modified_by}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Show Changes`)}
|
||||
value={diff_mode ? 'On' : 'Off'}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint react/no-unused-state: 0 */
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter, Redirect } from 'react-router-dom';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import { JobTemplatesAPI, ProjectsAPI } from '@api';
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
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 { TabbedCardHeader } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import ContentError from '@components/ContentError';
|
||||
import NotificationList from '@components/NotificationList';
|
||||
@ -121,10 +122,10 @@ class Template extends Component {
|
||||
});
|
||||
|
||||
let cardHeader = (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||
<CardCloseButton linkTo="/templates" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
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 { TabbedCardHeader } from '@components/Card';
|
||||
import AppendBody from '@components/AppendBody';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import ContentError from '@components/ContentError';
|
||||
@ -65,10 +66,10 @@ class WorkflowJobTemplate extends Component {
|
||||
});
|
||||
|
||||
let cardHeader = hasContentLoading ? null : (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||
<CardCloseButton linkTo="/templates" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
|
||||
import { CardBody } from '@components/Card';
|
||||
import { DetailList } from '@components/DetailList';
|
||||
|
||||
class WorkflowJobTemplateDetail extends Component {
|
||||
render() {
|
||||
return (
|
||||
<CardBody css="padding-top: 20px;">
|
||||
<CardBody>
|
||||
<DetailList gutter="sm" />
|
||||
</CardBody>
|
||||
);
|
||||
|
||||
@ -2,12 +2,8 @@ import React, { Component } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
CardHeader as PFCardHeader,
|
||||
PageSection,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { TabbedCardHeader } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import RoutedTabs from '@components/RoutedTabs';
|
||||
import ContentError from '@components/ContentError';
|
||||
@ -18,13 +14,6 @@ import UserTeams from './UserTeams';
|
||||
import UserTokens from './UserTokens';
|
||||
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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -90,7 +79,7 @@ class User extends Component {
|
||||
];
|
||||
|
||||
let cardHeader = (
|
||||
<CardHeader style={{ padding: 0 }}>
|
||||
<TabbedCardHeader>
|
||||
<RoutedTabs
|
||||
match={match}
|
||||
history={history}
|
||||
@ -98,7 +87,7 @@ class User extends Component {
|
||||
tabsArray={tabsArray}
|
||||
/>
|
||||
<CardCloseButton linkTo="/users" />
|
||||
</CardHeader>
|
||||
</TabbedCardHeader>
|
||||
);
|
||||
|
||||
if (!isInitialized) {
|
||||
|
||||
@ -5,11 +5,11 @@ import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Card as _Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
PageSection,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import UserForm from '../shared/UserForm';
|
||||
import { UsersAPI } from '@api';
|
||||
@ -41,7 +41,7 @@ function UserAdd({ history, i18n }) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardHeader css="text-align: right">
|
||||
<CardHeader className="at-u-textRight">
|
||||
<Tooltip content={i18n._(t`Close`)} position="top">
|
||||
<CardCloseButton onClick={handleCancel} />
|
||||
</Tooltip>
|
||||
|
||||
@ -2,16 +2,12 @@ import React, { Component } from 'react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { CardBody as PFCardBody, Button } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
|
||||
import { CardBody } from '@components/Card';
|
||||
import { DetailList, Detail } from '@components/DetailList';
|
||||
import { formatDateString } from '@util/dates';
|
||||
|
||||
const CardBody = styled(PFCardBody)`
|
||||
padding-top: 20px;
|
||||
`;
|
||||
|
||||
class UserDetail extends Component {
|
||||
render() {
|
||||
const {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
import UserForm from '../shared/UserForm';
|
||||
import { UsersAPI } from '@api';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class UserAdd extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class UserAdd extends Component {
|
||||
render() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '@patternfly/react-core';
|
||||
import { CardBody } from '@components/Card';
|
||||
|
||||
class UserAdd extends Component {
|
||||
render() {
|
||||
|
||||
@ -228,6 +228,14 @@ export const User = shape({
|
||||
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({
|
||||
id: number.isRequired,
|
||||
type: oneOf(['group']),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user