1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-31 15:21:13 +03:00

Start inventory detail

* Create VariablesDetail for read-only variables view
* Sketch out InventoryDetail
* Create CardBody and TabbedCardHeader for common custom styling
This commit is contained in:
Keith Grant 2019-12-12 10:36:21 -08:00
parent d0c891764f
commit 0ab61fd3cb
11 changed files with 224 additions and 59 deletions

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
import { CardBody as PFCardBody } from '@patternfly/react-core';
const CardBody = styled(PFCardBody)`
padding-top: 20px;
`;
PFCardBody.displayName = 'PFCardBody';
export default CardBody;

View File

@ -0,0 +1,11 @@
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;
position: relative;
`;
export default TabbedCardHeader;

View File

@ -0,0 +1,2 @@
export { default as CardBody } from './CardBody';
export { default as TabbedCardHeader } from './TabbedCardHeader';

View File

@ -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,
};

View File

@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { string, number } from 'prop-types';
import { Split, SplitItem } from '@patternfly/react-core';
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 [val, setVal] = useState(value);
const [error, setError] = useState(null);
return (
<div css="margin: 20px 0">
<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(val) : yamlToJson(val);
setVal(newVal);
setMode(newMode);
} catch (err) {
setError(err);
}
}}
/>
</SplitItem>
</Split>
<CodeMirrorInput
mode={mode}
value={val}
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>
)}
</div>
);
}
VariablesDetail.propTypes = {
value: string.isRequired,
label: string.isRequired,
rows: number,
};
VariablesDetail.defaultProps = {
rows: null,
};
export default VariablesDetail;

View File

@ -1,21 +1,16 @@
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 +25,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

View File

@ -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;

View File

@ -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';

View File

@ -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 style={{ padding: 0 }}>
<RoutedTabs history={history} tabsArray={tabsArray} />
<CardCloseButton linkTo="/inventories" />
</CardHeader>
</TabbedCardHeader>
);
if (

View File

@ -1,10 +1,56 @@
import React, { Component } from 'react';
import { CardBody } from '@patternfly/react-core';
import React, { useState, useEffect } from 'react';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { CardBody } from '@components/Card';
import { DetailList, Detail } from '@components/DetailList';
import { VariablesDetail } from '@components/CodeMirrorInput';
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 [isLoading, setIsLoading] = useState(false);
useEffect(() => {
(async () => {
setIsLoading(true);
const { data } = await InventoriesAPI.readInstanceGroups(inventory.id);
setInstanceGroups(data.results);
setIsLoading(false);
})();
}, [inventory.id]);
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={inventory.kind} />
<Detail
label={i18n._(t`Organization`)}
value={inventory.summary_fields.organization.name}
/>
<Detail
fullWidth
label={i18n._(t`Instance Groups`)}
value={isLoading ? 'loading...' : instanceGroups.map(g => g.name)}
/>
</DetailList>
{inventory.variables && (
<VariablesDetail
id="job-artifacts"
value={inventory.variables}
rows={4}
label={i18n._(t`Variables`)}
/>
)}{' '}
<Detail label={i18n._(t`Description`)} value={inventory.description} />
</CardBody>
);
}
InventoryDetail.propTypes = {
inventory: Inventory.isRequired,
};
export default InventoryDetail;
export default withI18n()(InventoryDetail);

View File

@ -58,7 +58,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 +78,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) {