diff --git a/awx/ui_next/src/components/Card/CardBody.jsx b/awx/ui_next/src/components/Card/CardBody.jsx
new file mode 100644
index 0000000000..43120cd17d
--- /dev/null
+++ b/awx/ui_next/src/components/Card/CardBody.jsx
@@ -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;
diff --git a/awx/ui_next/src/components/Card/TabbedCardHeader.js b/awx/ui_next/src/components/Card/TabbedCardHeader.js
new file mode 100644
index 0000000000..0a86d11a59
--- /dev/null
+++ b/awx/ui_next/src/components/Card/TabbedCardHeader.js
@@ -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;
diff --git a/awx/ui_next/src/components/Card/index.js b/awx/ui_next/src/components/Card/index.js
new file mode 100644
index 0000000000..617e65871f
--- /dev/null
+++ b/awx/ui_next/src/components/Card/index.js
@@ -0,0 +1,2 @@
+export { default as CardBody } from './CardBody';
+export { default as TabbedCardHeader } from './TabbedCardHeader';
diff --git a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.jsx
index 6513322f66..c84ff0ba84 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/CodeMirrorInput.jsx
@@ -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,
};
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
new file mode 100644
index 0000000000..04c7a1ed4b
--- /dev/null
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx
@@ -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 (
+
+
+
+
+
+ {label}
+
+
+
+
+ {
+ try {
+ const newVal =
+ newMode === YAML_MODE ? jsonToYaml(val) : yamlToJson(val);
+ setVal(newVal);
+ setMode(newMode);
+ } catch (err) {
+ setError(err);
+ }
+ }}
+ />
+
+
+
+ {error && (
+
+ Error: {error.message}
+
+ )}
+
+ );
+}
+VariablesDetail.propTypes = {
+ value: string.isRequired,
+ label: string.isRequired,
+ rows: number,
+};
+VariablesDetail.defaultProps = {
+ rows: null,
+};
+
+export default VariablesDetail;
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
index 3c828c6308..96b516946e 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
+++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx
@@ -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 }) {
-
- {
- 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
-
- {
- 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
-
-
+ {
+ 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);
+ }
+ }}
+ />
{
+ if (mode !== newMode) {
+ onChange(newMode);
+ }
+ };
+
+ return (
+
+ setMode(YAML_MODE)}
+ variant={mode === YAML_MODE ? 'primary' : 'secondary'}
+ >
+ YAML
+
+ setMode(JSON_MODE)}
+ variant={mode === JSON_MODE ? 'primary' : 'secondary'}
+ >
+ JSON
+
+
+ );
+}
+YamlJsonToggle.propTypes = {
+ mode: oneOf([YAML_MODE, JSON_MODE]).isRequired,
+ onChange: func.isRequired,
+};
+
+export default YamlJsonToggle;
diff --git a/awx/ui_next/src/components/CodeMirrorInput/index.js b/awx/ui_next/src/components/CodeMirrorInput/index.js
index 48940bbb37..9cad016228 100644
--- a/awx/ui_next/src/components/CodeMirrorInput/index.js
+++ b/awx/ui_next/src/components/CodeMirrorInput/index.js
@@ -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';
diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx
index f3e78d584b..03fd08d3fe 100644
--- a/awx/ui_next/src/screens/Inventory/Inventory.jsx
+++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx
@@ -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 : (
-
+
-
+
);
if (
diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
index 5d9d9789b6..35c6830818 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx
@@ -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 Coming soon :);
- }
+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 (
+
+
+
+
+
+
+
+ g.name)}
+ />
+
+ {inventory.variables && (
+
+ )}{' '}
+
+
+ );
}
+InventoryDetail.propTypes = {
+ inventory: Inventory.isRequired,
+};
-export default InventoryDetail;
+export default withI18n()(InventoryDetail);
diff --git a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx
index 2ec78aef4a..ea1d7a8088 100644
--- a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx
@@ -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) {