From 0ab61fd3cbe2a48940b4578a9dd9b62e8ff49627 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 12 Dec 2019 10:36:21 -0800 Subject: [PATCH 01/13] Start inventory detail * Create VariablesDetail for read-only variables view * Sketch out InventoryDetail * Create CardBody and TabbedCardHeader for common custom styling --- awx/ui_next/src/components/Card/CardBody.jsx | 9 +++ .../src/components/Card/TabbedCardHeader.js | 11 +++ awx/ui_next/src/components/Card/index.js | 2 + .../CodeMirrorInput/CodeMirrorInput.jsx | 3 +- .../CodeMirrorInput/VariablesDetail.jsx | 72 +++++++++++++++++++ .../CodeMirrorInput/VariablesField.jsx | 60 +++++----------- .../CodeMirrorInput/YamlJsonToggle.jsx | 44 ++++++++++++ .../src/components/CodeMirrorInput/index.js | 1 + .../src/screens/Inventory/Inventory.jsx | 7 +- .../InventoryDetail/InventoryDetail.jsx | 60 ++++++++++++++-- .../Inventory/InventoryEdit/InventoryEdit.jsx | 14 ++-- 11 files changed, 224 insertions(+), 59 deletions(-) create mode 100644 awx/ui_next/src/components/Card/CardBody.jsx create mode 100644 awx/ui_next/src/components/Card/TabbedCardHeader.js create mode 100644 awx/ui_next/src/components/Card/index.js create mode 100644 awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx create mode 100644 awx/ui_next/src/components/CodeMirrorInput/YamlJsonToggle.jsx 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) { From 3d45f275025d003f818b2104a54be6b3437de595 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Fri, 13 Dec 2019 16:10:37 -0800 Subject: [PATCH 02/13] finish InventoryDetail --- .../components/DetailList/UserDateDetail.jsx | 31 +++++++++++++ .../src/components/DetailList/index.js | 1 + .../InventoryDetail/InventoryDetail.jsx | 45 ++++++++++++++++--- awx/ui_next/src/types.js | 8 ++++ 4 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 awx/ui_next/src/components/DetailList/UserDateDetail.jsx diff --git a/awx/ui_next/src/components/DetailList/UserDateDetail.jsx b/awx/ui_next/src/components/DetailList/UserDateDetail.jsx new file mode 100644 index 0000000000..b3d4b320be --- /dev/null +++ b/awx/ui_next/src/components/DetailList/UserDateDetail.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { node, string } from 'prop-types'; +import { Link } from 'react-router-dom'; +import { formatDateString } from '@util/dates'; +import Detail from './Detail'; +import { SummaryFieldUser } from '../../types'; + +function UserDateDetail({ label, date, user }) { + return ( + + {formatDateString(date)} + {user && ' by '} + {user && {user.username}} + + } + /> + ); +} +UserDateDetail.propTypes = { + label: node.isRequired, + date: string.isRequired, + user: SummaryFieldUser, +}; +UserDateDetail.defaultProps = { + user: null, +}; + +export default UserDateDetail; diff --git a/awx/ui_next/src/components/DetailList/index.js b/awx/ui_next/src/components/DetailList/index.js index 665470d178..4a5b77dbc0 100644 --- a/awx/ui_next/src/components/DetailList/index.js +++ b/awx/ui_next/src/components/DetailList/index.js @@ -1,2 +1,3 @@ export { default as DetailList } from './DetailList'; export { default as Detail, DetailName, DetailValue } from './Detail'; +export { default as UserDateDetail } from './UserDateDetail'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx index 35c6830818..60ccec382f 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx @@ -1,8 +1,10 @@ import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { CardBody } from '@components/Card'; -import { DetailList, Detail } from '@components/DetailList'; +import { DetailList, Detail, UserDateDetail } from '@components/DetailList'; +import { ChipGroup, Chip } from '@components/Chip'; import { VariablesDetail } from '@components/CodeMirrorInput'; import { InventoriesAPI } from '@api'; import { Inventory } from '../../../types'; @@ -20,32 +22,61 @@ function InventoryDetail({ inventory, i18n }) { })(); }, [inventory.id]); + const { organization } = inventory.summary_fields; + return ( - + + {organization.name} + + } /> g.name)} + value={ + isLoading ? ( + 'loading...' + ) : ( + + {instanceGroups.map(ig => ( + + {ig.name} + + ))} + + ) + } /> {inventory.variables && ( - )}{' '} - + )} + + + + ); } diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 5fee265305..0b6542f3ac 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -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']), From 41c9ea3c07c0a8b96738ec4cf3ec62cd57625fc9 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 16 Dec 2019 10:50:25 -0800 Subject: [PATCH 03/13] add tests for VariablesDetail & InventoryDetail --- .../CodeMirrorInput/VariablesDetail.test.jsx | 43 ++++++ .../InventoryDetail/InventoryDetail.test.jsx | 122 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx new file mode 100644 index 0000000000..71d082290c --- /dev/null +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.test.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VariablesDetail from './VariablesDetail'; + +jest.mock('@api'); + +describe('', () => { + test('should render readonly CodeMirrorInput', () => { + const wrapper = shallow( + + ); + 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( + + ); + 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( + + ); + 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'); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx new file mode 100644 index 0000000000..05c9c57988 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx @@ -0,0 +1,122 @@ +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('', () => { + test('should render details', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + 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(` + + The Organization + + `); + 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( + + ); + }); + 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'); + }); +}); From cde39413c92b8929a82f98366262ed71c2ac69e4 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 16 Dec 2019 15:13:10 -0800 Subject: [PATCH 04/13] switch all tabbed screens to use TabbedCardHeader --- awx/ui_next/src/components/Card/CardBody.jsx | 9 --------- .../src/components/Card/TabbedCardHeader.js | 1 + awx/ui_next/src/components/Card/index.js | 1 - awx/ui_next/src/screens/Host/Host.jsx | 19 ++++--------------- .../screens/Host/HostDetail/HostDetail.jsx | 2 +- .../src/screens/Inventory/Inventory.jsx | 2 +- .../InventoryDetail/InventoryDetail.jsx | 2 +- .../src/screens/Inventory/SmartInventory.jsx | 7 ++++--- awx/ui_next/src/screens/Job/Job.jsx | 19 ++++--------------- .../src/screens/Organization/Organization.jsx | 19 ++++--------------- awx/ui_next/src/screens/Project/Project.jsx | 19 ++++--------------- .../screens/Project/ProjectAdd/ProjectAdd.jsx | 2 +- .../Project/ProjectDetail/ProjectDetail.jsx | 2 +- .../Project/ProjectEdit/ProjectEdit.jsx | 2 +- awx/ui_next/src/screens/Team/Team.jsx | 19 ++++--------------- .../JobTemplateDetail/JobTemplateDetail.jsx | 2 +- awx/ui_next/src/screens/Template/Template.jsx | 7 ++++--- .../screens/Template/WorkflowJobTemplate.jsx | 7 ++++--- .../WorkflowJobTemplateDetail.jsx | 2 +- awx/ui_next/src/screens/User/User.jsx | 19 ++++--------------- .../src/screens/User/UserAdd/UserAdd.jsx | 2 +- 21 files changed, 46 insertions(+), 118 deletions(-) delete mode 100644 awx/ui_next/src/components/Card/CardBody.jsx diff --git a/awx/ui_next/src/components/Card/CardBody.jsx b/awx/ui_next/src/components/Card/CardBody.jsx deleted file mode 100644 index 43120cd17d..0000000000 --- a/awx/ui_next/src/components/Card/CardBody.jsx +++ /dev/null @@ -1,9 +0,0 @@ -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 index 0a86d11a59..3878449307 100644 --- a/awx/ui_next/src/components/Card/TabbedCardHeader.js +++ b/awx/ui_next/src/components/Card/TabbedCardHeader.js @@ -5,6 +5,7 @@ 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; position: relative; `; diff --git a/awx/ui_next/src/components/Card/index.js b/awx/ui_next/src/components/Card/index.js index 617e65871f..6d197d5600 100644 --- a/awx/ui_next/src/components/Card/index.js +++ b/awx/ui_next/src/components/Card/index.js @@ -1,2 +1 @@ -export { default as CardBody } from './CardBody'; export { default as TabbedCardHeader } from './TabbedCardHeader'; diff --git a/awx/ui_next/src/screens/Host/Host.jsx b/awx/ui_next/src/screens/Host/Host.jsx index 20cd4aad0c..0601e661a8 100644 --- a/awx/ui_next/src/screens/Host/Host.jsx +++ b/awx/ui_next/src/screens/Host/Host.jsx @@ -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 = ( - + - + ); if (!isInitialized) { diff --git a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx index 479d28211f..e9e18c3c4f 100644 --- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx +++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx @@ -46,7 +46,7 @@ function HostDetail({ host, i18n }) { } return ( - + diff --git a/awx/ui_next/src/screens/Inventory/Inventory.jsx b/awx/ui_next/src/screens/Inventory/Inventory.jsx index 03fd08d3fe..e632610660 100644 --- a/awx/ui_next/src/screens/Inventory/Inventory.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventory.jsx @@ -52,7 +52,7 @@ function Inventory({ history, i18n, location, match, setBreadcrumb }) { ]; let cardHeader = hasContentLoading ? null : ( - + diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx index 60ccec382f..dffff1881c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { CardBody } from '@components/Card'; +import { CardBody } from '@patternfly/react-core'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList'; import { ChipGroup, Chip } from '@components/Chip'; import { VariablesDetail } from '@components/CodeMirrorInput'; diff --git a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx index 2acd518b2c..7328f79361 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx @@ -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 : ( - + - + ); if (location.pathname.endsWith('edit')) { diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index 2178bfc2cf..51032d7553 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -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 = ( - + - + ); if (!isInitialized) { diff --git a/awx/ui_next/src/screens/Organization/Organization.jsx b/awx/ui_next/src/screens/Organization/Organization.jsx index 3e7dfaa0af..66679cced5 100644 --- a/awx/ui_next/src/screens/Organization/Organization.jsx +++ b/awx/ui_next/src/screens/Organization/Organization.jsx @@ -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 = ( - + - + ); if (!isInitialized) { diff --git a/awx/ui_next/src/screens/Project/Project.jsx b/awx/ui_next/src/screens/Project/Project.jsx index a410edd562..1215586ea2 100644 --- a/awx/ui_next/src/screens/Project/Project.jsx +++ b/awx/ui_next/src/screens/Project/Project.jsx @@ -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 = ( - + - + ); if (!isInitialized) { diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx index 5eeb4eeb45..7048170166 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.jsx @@ -44,7 +44,7 @@ function ProjectAdd({ history, i18n }) { return ( - + diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index 25548e0a2e..855c7097ab 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -89,7 +89,7 @@ function ProjectDetail({ project, i18n }) { } return ( - + - + diff --git a/awx/ui_next/src/screens/Team/Team.jsx b/awx/ui_next/src/screens/Team/Team.jsx index 9aabb7b1dd..f44d71c366 100644 --- a/awx/ui_next/src/screens/Team/Team.jsx +++ b/awx/ui_next/src/screens/Team/Team.jsx @@ -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 = ( - + - + ); if (!isInitialized) { diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index fbd5d9e1bf..6d9b9d09dc 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -195,7 +195,7 @@ class JobTemplateDetail extends Component { return ( isInitialized && ( - + + - + ); if (location.pathname.endsWith('edit')) { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx index 30371f1dd1..fadeb5878c 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx @@ -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 : ( - + - + ); if (location.pathname.endsWith('edit')) { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx index a1a6c7cd58..3cd04bf305 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx @@ -8,7 +8,7 @@ import { DetailList } from '@components/DetailList'; class WorkflowJobTemplateDetail extends Component { render() { return ( - + ); diff --git a/awx/ui_next/src/screens/User/User.jsx b/awx/ui_next/src/screens/User/User.jsx index c4ee4c5fb8..697976cd55 100644 --- a/awx/ui_next/src/screens/User/User.jsx +++ b/awx/ui_next/src/screens/User/User.jsx @@ -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 = ( - + - + ); if (!isInitialized) { diff --git a/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx b/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx index 4b9587086e..3112db6c8b 100644 --- a/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx +++ b/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx @@ -41,7 +41,7 @@ function UserAdd({ history, i18n }) { return ( - + From 2f7607a080e1e591c7ffbe2b373e927fafbbd3b0 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 16 Dec 2019 16:11:24 -0800 Subject: [PATCH 05/13] use VariablesDetail for displaying variables field in details views --- awx/ui_next/src/components/Card/index.js | 1 + .../CodeMirrorInput/VariablesDetail.jsx | 105 ++++++++++-------- .../CodeMirrorInput/VariablesField.jsx | 1 - .../screens/Host/HostDetail/HostDetail.jsx | 58 +++------- .../Host/HostDetail/HostDetail.test.jsx | 15 ++- .../InventoryDetail/InventoryDetail.jsx | 17 ++- 6 files changed, 90 insertions(+), 107 deletions(-) diff --git a/awx/ui_next/src/components/Card/index.js b/awx/ui_next/src/components/Card/index.js index 6d197d5600..55c70b8e14 100644 --- a/awx/ui_next/src/components/Card/index.js +++ b/awx/ui_next/src/components/Card/index.js @@ -1 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export export { default as TabbedCardHeader } from './TabbedCardHeader'; diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx index 04c7a1ed4b..2ea8e15ff9 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { string, number } from 'prop-types'; -import { Split, SplitItem } from '@patternfly/react-core'; +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'; @@ -10,54 +11,68 @@ 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 [currentValue, setCurrentValue] = 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 && ( -
+ + +
+ + {label} + +
+
+ + { + try { + const newVal = + newMode === YAML_MODE + ? jsonToYaml(currentValue) + : yamlToJson(currentValue); + setCurrentValue(newVal); + setMode(newMode); + } catch (err) { + setError(err); + } + }} + /> + +
+ + + + {error && ( +
- Error: {error.message} -
- )} -
+ > + Error: {error.message} +
+ )} + + ); } VariablesDetail.propTypes = { diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx index 96b516946e..97c35c1871 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx @@ -7,7 +7,6 @@ import YamlJsonToggle from './YamlJsonToggle'; import { yamlToJson, jsonToYaml } from '../../util/yaml'; const YAML_MODE = 'yaml'; -const JSON_MODE = 'javascript'; function VariablesField({ id, name, label, readOnly }) { // TODO: detect initial mode diff --git a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx index e9e18c3c4f..cf026f40f7 100644 --- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx +++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.jsx @@ -4,10 +4,9 @@ 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 { DetailList, Detail, UserDateDetail } from '@components/DetailList'; +import { VariablesDetail } from '@components/CodeMirrorInput'; const ActionButtonWrapper = styled.div` display: flex; @@ -21,30 +20,6 @@ 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 ( @@ -66,23 +41,20 @@ function HostDetail({ host, i18n }) { } /> )} - {/* TODO: Link to user in users */} - - {/* TODO: Link to user in users */} - - + + {}} - rows={6} - hasErrors={false} - /> - } + value={host.variables} + rows={6} /> diff --git a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx index a748f9596d..749c512171 100644 --- a/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx +++ b/awx/ui_next/src/screens/Host/HostDetail/HostDetail.test.jsx @@ -30,7 +30,7 @@ describe('', () => { mountWithContexts(); }); - test('should render Details', async done => { + test('should render Details', async () => { const wrapper = mountWithContexts(); const testParams = [ { label: 'Name', value: 'Foo' }, @@ -46,23 +46,22 @@ describe('', () => { 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(); - 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(); await waitForElement(wrapper, 'HostDetail'); - expect(wrapper.find('HostDetail Button').length).toBe(0); - done(); + // VariablesDetail has two buttons + expect(wrapper.find('Button').length).toBe(2); }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx index dffff1881c..933b8794f3 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx @@ -56,16 +56,13 @@ function InventoryDetail({ inventory, i18n }) { ) } /> -
- {inventory.variables && ( - - )} - + {inventory.variables && ( + + )} Date: Tue, 17 Dec 2019 12:39:42 -0800 Subject: [PATCH 06/13] use UserDateDetail in OrganizationDetail --- .../OrganizationDetail/OrganizationDetail.jsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx index 29fe94295e..0c98337ac8 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx @@ -2,19 +2,12 @@ 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 { CardBody, Button } from '@patternfly/react-core'; import { OrganizationsAPI } from '@api'; -import { DetailList, Detail } from '@components/DetailList'; +import { DetailList, Detail, UserDateDetail } from '@components/DetailList'; 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 +65,15 @@ function OrganizationDetail({ i18n, organization }) { label={i18n._(t`Ansible Environment`)} value={custom_virtualenv} /> - - + {instanceGroups && instanceGroups.length > 0 && ( Date: Wed, 18 Dec 2019 09:47:04 -0800 Subject: [PATCH 07/13] InventoryDetail: handle content loading state & errors better --- .../InventoryDetail/InventoryDetail.jsx | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx index 933b8794f3..d3e67e2da5 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx @@ -6,24 +6,40 @@ import { CardBody } from '@patternfly/react-core'; import { DetailList, Detail, UserDateDetail } from '@components/DetailList'; import { ChipGroup, Chip } from '@components/Chip'; import { VariablesDetail } from '@components/CodeMirrorInput'; +import ContentError from '@components/ContentError'; +import ContentLoading from '@components/ContentLoading'; import { InventoriesAPI } from '@api'; import { Inventory } from '../../../types'; function InventoryDetail({ inventory, i18n }) { const [instanceGroups, setInstanceGroups] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [hasContentLoading, setHasContentLoading] = useState(false); + const [contentError, setContentError] = useState(null); useEffect(() => { (async () => { - setIsLoading(true); - const { data } = await InventoriesAPI.readInstanceGroups(inventory.id); - setInstanceGroups(data.results); - setIsLoading(false); + setHasContentLoading(true); + try { + const { data } = await InventoriesAPI.readInstanceGroups(inventory.id); + setInstanceGroups(data.results); + } catch (err) { + setContentError(err); + } finally { + setHasContentLoading(false); + } })(); }, [inventory.id]); const { organization } = inventory.summary_fields; + if (hasContentLoading) { + return ; + } + + if (contentError) { + return ; + } + return ( @@ -43,17 +59,13 @@ function InventoryDetail({ inventory, i18n }) { fullWidth label={i18n._(t`Instance Groups`)} value={ - isLoading ? ( - 'loading...' - ) : ( - - {instanceGroups.map(ig => ( - - {ig.name} - - ))} - - ) + + {instanceGroups.map(ig => ( + + {ig.name} + + ))} + } /> {inventory.variables && ( From 8ff09021779e4f162292517ea2ec67b59a464dea Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Wed, 18 Dec 2019 11:44:38 -0800 Subject: [PATCH 08/13] Fix UserDateDetail translation Add UserDateDetail to Org detail & InventoryGroupDetail Add VariablesDetail to InventoryGroupDetail --- awx/ui_next/CONTRIBUTING.md | 4 +- .../CodeMirrorInput/VariablesDetail.jsx | 4 + .../components/DetailList/UserDateDetail.jsx | 15 ++-- .../InventoryDetail/InventoryDetail.jsx | 12 ++- .../InventoryGroup/InventoryGroup.jsx | 6 +- .../InventoryGroupDetail.jsx | 74 ++++++------------- .../Project/ProjectDetail/ProjectDetail.jsx | 41 +++------- .../JobTemplateDetail/JobTemplateDetail.jsx | 51 +++---------- 8 files changed, 67 insertions(+), 140 deletions(-) diff --git a/awx/ui_next/CONTRIBUTING.md b/awx/ui_next/CONTRIBUTING.md index 749dc8ab20..105935734f 100644 --- a/awx/ui_next/CONTRIBUTING.md +++ b/awx/ui_next/CONTRIBUTING.md @@ -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 `` 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 `` 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). diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx index 2ea8e15ff9..d89ea07b58 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx @@ -14,6 +14,10 @@ function VariablesDetail({ value, label, rows }) { const [currentValue, setCurrentValue] = useState(value); const [error, setError] = useState(null); + if (!value) { + return null; + } + return ( <> - {formatDateString(date)} - {user && ' by '} - {user && {user.username}} - + user ? ( + + {dateStr} by {username} + + ) : ( + dateStr + ) } /> ); diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx index d3e67e2da5..c0628b663c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx @@ -68,13 +68,11 @@ function InventoryDetail({ inventory, i18n }) { } /> - {inventory.variables && ( - - )} + + -
+ ); } return ( diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx index 8cf97cffc0..8f09750642 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.jsx @@ -3,23 +3,16 @@ import { t } from '@lingui/macro'; import { CardBody, 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 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 +21,7 @@ const ActionButtonWrapper = styled.div` margin-left: 20px; } `; + function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { const { summary_fields: { created_by, modified_by }, @@ -50,52 +44,26 @@ function InventoryGroupDetail({ i18n, history, match, inventoryGroup }) { } }; - let createdBy = ''; - if (created) { - if (created_by && created_by.username) { - createdBy = ( - - {i18n._(t`${formatDateString(inventoryGroup.created)} by`)}{' '} - {created_by.username} - - ); - } else { - createdBy = formatDateString(inventoryGroup.created); - } - } - - let modifiedBy = ''; - if (modified) { - if (modified_by && modified_by.username) { - modifiedBy = ( - - {i18n._(t`${formatDateString(inventoryGroup.modified)} by`)}{' '} - {modified_by.username} - - ); - } else { - modifiedBy = formatDateString(inventoryGroup.modified); - } - } - return ( - + - - - - {createdBy && } - {modifiedBy && ( - - )} + + + + + )} ); } From bfedbe561c37174dc7d3f4c902bce665e0f10b8b Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 19 Dec 2019 09:55:04 -0800 Subject: [PATCH 11/13] add delete button to InventoryDetail --- .../src/components/Card/CardActionsRow.jsx | 4 +- .../components/DeleteButton/DeleteButton.jsx | 50 +++++++++++++++++++ .../src/components/DeleteButton/index.js | 1 + .../InventoryDetail/InventoryDetail.jsx | 31 +++++++++--- 4 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 awx/ui_next/src/components/DeleteButton/DeleteButton.jsx create mode 100644 awx/ui_next/src/components/DeleteButton/index.js diff --git a/awx/ui_next/src/components/Card/CardActionsRow.jsx b/awx/ui_next/src/components/Card/CardActionsRow.jsx index 045483a06a..8312207fa9 100644 --- a/awx/ui_next/src/components/Card/CardActionsRow.jsx +++ b/awx/ui_next/src/components/Card/CardActionsRow.jsx @@ -7,8 +7,8 @@ const CardActionsWrapper = styled.div` justify-content: flex-end; margin-top: 20px; - & > :not(:first-child) { - margin-left: 20px; + & > .pf-c-card__actions > :not(:first-child) { + margin-left: 0.5rem; } `; diff --git a/awx/ui_next/src/components/DeleteButton/DeleteButton.jsx b/awx/ui_next/src/components/DeleteButton/DeleteButton.jsx new file mode 100644 index 0000000000..69a84e7f79 --- /dev/null +++ b/awx/ui_next/src/components/DeleteButton/DeleteButton.jsx @@ -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 ( + <> + + setIsOpen(false)} + > + {i18n._(t`Are you sure you want to delete:`)} +
+ {name} + + + + +
+ + ); +} + +export default withI18n()(DeleteButton); diff --git a/awx/ui_next/src/components/DeleteButton/index.js b/awx/ui_next/src/components/DeleteButton/index.js new file mode 100644 index 0000000000..991ad79b29 --- /dev/null +++ b/awx/ui_next/src/components/DeleteButton/index.js @@ -0,0 +1 @@ +export { default } from './DeleteButton'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx index 182978fd21..81a765982a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Button } from '@patternfly/react-core'; @@ -7,6 +7,7 @@ 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'; @@ -16,6 +17,7 @@ function InventoryDetail({ inventory, i18n }) { const [instanceGroups, setInstanceGroups] = useState([]); const [hasContentLoading, setHasContentLoading] = useState(true); const [contentError, setContentError] = useState(null); + const history = useHistory(); useEffect(() => { (async () => { @@ -31,7 +33,15 @@ function InventoryDetail({ inventory, i18n }) { })(); }, [inventory.id]); - const { organization } = inventory.summary_fields; + const deleteInventory = async () => { + await InventoriesAPI.destroy(inventory.id); + history.push(`/inventories`); + }; + + const { + organization, + user_capabilities: userCapabilities, + } = inventory.summary_fields; if (hasContentLoading) { return ; @@ -85,16 +95,25 @@ function InventoryDetail({ inventory, i18n }) { user={inventory.summary_fields.modified_by} />
- {inventory.summary_fields.user_capabilities.edit && ( - + + {userCapabilities.edit && ( - - )} + )} + {userCapabilities.delete && ( + + {i18n._(t`Delete`)} + + )} +
); } From e688ed813ac52e063efbfe821a88cf86d79c8c4e Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 19 Dec 2019 10:41:59 -0800 Subject: [PATCH 12/13] update tests for detail view changes --- .../InventoryDetail/InventoryDetail.test.jsx | 4 +++- .../InventoryGroupDetail.test.jsx | 4 ++-- .../Project/ProjectDetail/ProjectDetail.test.jsx | 14 ++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx index 05c9c57988..97e835d996 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx @@ -1,7 +1,8 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { InventoriesAPI, CredentialTypesAPI } from '@api'; +import { sleep } from '@testUtils/testUtils'; import InventoryDetail from './InventoryDetail'; jest.mock('@api'); @@ -83,6 +84,7 @@ describe('', () => { ); }); + wrapper.update(); expectDetailToMatch(wrapper, 'Name', mockInventory.name); expectDetailToMatch(wrapper, 'Activity', 'Coming soon'); expectDetailToMatch(wrapper, 'Description', mockInventory.description); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx index 99a017ce32..54e660ccc2 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupDetail/InventoryGroupDetail.test.jsx @@ -80,7 +80,7 @@ describe('', () => { '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'); }); }); diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx index ef62183a9f..d0eb66ca66 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx @@ -98,13 +98,15 @@ describe('', () => { `${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 From b794fdbefdaa7b66d13bb339bce56480c2f398be Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 19 Dec 2019 10:52:23 -0800 Subject: [PATCH 13/13] de-lint --- .../screens/Inventory/InventoryDetail/InventoryDetail.test.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx index 97e835d996..9f3a0bba26 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryDetail/InventoryDetail.test.jsx @@ -1,8 +1,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { InventoriesAPI, CredentialTypesAPI } from '@api'; -import { sleep } from '@testUtils/testUtils'; import InventoryDetail from './InventoryDetail'; jest.mock('@api');