From 90edb3b551f52e036681513c857ecfbe227b27d5 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 14 Jan 2021 09:42:12 -0500 Subject: [PATCH 01/17] update Breadcrumb component to ScreenHeader: - show last breadcrum item as Title on new line - add activity stream type (to display activity stream icon link in header) --- .../components/Breadcrumbs/Breadcrumbs.jsx | 73 ---------- .../src/components/Breadcrumbs/index.js | 1 - .../components/ScreenHeader/ScreenHeader.jsx | 129 ++++++++++++++++++ .../ScreenHeader.test.jsx} | 24 ++-- .../src/components/ScreenHeader/index.js | 1 + .../src/screens/Application/Applications.jsx | 7 +- .../screens/Application/Applications.test.jsx | 4 + .../src/screens/Credential/Credentials.jsx | 7 +- .../screens/Credential/Credentials.test.jsx | 12 +- .../CredentialType/CredentialTypes.jsx | 7 +- .../CredentialType/CredentialTypes.test.jsx | 4 + .../src/screens/Dashboard/Dashboard.jsx | 7 +- .../src/screens/Dashboard/Dashboard.test.jsx | 3 + awx/ui_next/src/screens/Host/Hosts.jsx | 4 +- awx/ui_next/src/screens/Host/Hosts.test.jsx | 6 +- .../screens/InstanceGroup/InstanceGroups.jsx | 7 +- .../InstanceGroup/InstanceGroups.test.jsx | 4 + .../src/screens/Inventory/Inventories.jsx | 7 +- .../screens/Inventory/Inventories.test.jsx | 4 + awx/ui_next/src/screens/Job/Job.test.jsx | 4 + awx/ui_next/src/screens/Job/Jobs.jsx | 4 +- awx/ui_next/src/screens/Job/Jobs.test.jsx | 6 +- .../screens/ManagementJob/ManagementJobs.jsx | 5 +- .../ManagementJob/ManagementJobs.test.jsx | 6 +- .../NotificationTemplates.jsx | 7 +- .../NotificationTemplates.test.jsx | 4 + .../screens/Organization/Organizations.jsx | 7 +- .../Organization/Organizations.test.jsx | 3 + awx/ui_next/src/screens/Project/Projects.jsx | 4 +- .../src/screens/Project/Projects.test.jsx | 6 +- .../src/screens/Schedule/AllSchedules.jsx | 5 +- .../screens/Schedule/AllSchedules.test.jsx | 7 +- awx/ui_next/src/screens/Setting/Settings.jsx | 4 +- .../src/screens/Setting/Settings.test.jsx | 3 + awx/ui_next/src/screens/Team/Teams.jsx | 4 +- awx/ui_next/src/screens/Team/Teams.test.jsx | 3 + .../src/screens/Template/Templates.jsx | 17 ++- .../src/screens/Template/Templates.test.jsx | 4 + awx/ui_next/src/screens/User/Users.jsx | 4 +- awx/ui_next/src/screens/User/Users.test.jsx | 6 +- .../WorkflowApproval/WorkflowApprovals.jsx | 7 +- .../WorkflowApprovals.test.jsx | 7 +- 42 files changed, 302 insertions(+), 136 deletions(-) delete mode 100644 awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx delete mode 100644 awx/ui_next/src/components/Breadcrumbs/index.js create mode 100644 awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx rename awx/ui_next/src/components/{Breadcrumbs/Breadcrumbs.test.jsx => ScreenHeader/ScreenHeader.test.jsx} (70%) create mode 100644 awx/ui_next/src/components/ScreenHeader/index.js diff --git a/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx b/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx deleted file mode 100644 index 93a9b3d7f4..0000000000 --- a/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - PageSection as PFPageSection, - PageSectionVariants, - Breadcrumb, - BreadcrumbItem, - BreadcrumbHeading, -} from '@patternfly/react-core'; -import { Link, Route, useRouteMatch } from 'react-router-dom'; - -import styled from 'styled-components'; - -const PageSection = styled(PFPageSection)` - padding-top: 10px; - padding-bottom: 10px; -`; - -const Breadcrumbs = ({ breadcrumbConfig }) => { - const { light } = PageSectionVariants; - - return ( - - - - - - - - ); -}; - -const Crumb = ({ breadcrumbConfig, showDivider }) => { - const match = useRouteMatch(); - const crumb = breadcrumbConfig[match.url]; - - let crumbElement = ( - - {crumb} - - ); - - if (match.isExact) { - crumbElement = ( - - {crumb} - - ); - } - - if (!crumb) { - crumbElement = null; - } - - return ( - - {crumbElement} - - - - - ); -}; - -Breadcrumbs.propTypes = { - breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired, -}; - -Crumb.propTypes = { - breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired, -}; - -export default Breadcrumbs; diff --git a/awx/ui_next/src/components/Breadcrumbs/index.js b/awx/ui_next/src/components/Breadcrumbs/index.js deleted file mode 100644 index 3ff68ca589..0000000000 --- a/awx/ui_next/src/components/Breadcrumbs/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Breadcrumbs'; diff --git a/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx new file mode 100644 index 0000000000..791e72b78e --- /dev/null +++ b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx @@ -0,0 +1,129 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + Button, + PageSection, + PageSectionVariants, + Breadcrumb, + BreadcrumbItem, + Title, + Tooltip, +} from '@patternfly/react-core'; +import { HistoryIcon } from '@patternfly/react-icons'; +import { Link, Route, useRouteMatch } from 'react-router-dom'; + +const ScreenHeader = ({ breadcrumbConfig, i18n, streamType }) => { + const { light } = PageSectionVariants; + const oneCrumbMatch = useRouteMatch({ + path: Object.keys(breadcrumbConfig)[0], + strict: true, + }); + const isOnlyOneCrumb = oneCrumbMatch && oneCrumbMatch.isExact; + + return ( + +
+
+ {!isOnlyOneCrumb && ( + + + + + + )} + + + +
+ {streamType !== 'none' && ( +
+ + + +
+ )} +
+
+ ); +}; + +const ActualTitle = ({ breadcrumbConfig }) => { + const match = useRouteMatch(); + const title = breadcrumbConfig[match.url]; + let titleElement; + + if (match.isExact) { + titleElement = ( + + {title} + + ); + } + + if (!title) { + titleElement = null; + } + + return ( + + {titleElement} + + + + + ); +}; + +const Crumb = ({ breadcrumbConfig, showDivider }) => { + const match = useRouteMatch(); + const crumb = breadcrumbConfig[match.url]; + + let crumbElement = ( + + {crumb} + + ); + + if (match.isExact) { + crumbElement = null; + } + + if (!crumb) { + crumbElement = null; + } + return ( + + {crumbElement} + + + + + ); +}; + +ScreenHeader.propTypes = { + breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired, +}; + +Crumb.propTypes = { + breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired, +}; + +export default withI18n()(ScreenHeader); diff --git a/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.test.jsx b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.test.jsx similarity index 70% rename from awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.test.jsx rename to awx/ui_next/src/components/ScreenHeader/ScreenHeader.test.jsx index 83e5e8c3fe..64f3a92c53 100644 --- a/awx/ui_next/src/components/Breadcrumbs/Breadcrumbs.test.jsx +++ b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.test.jsx @@ -1,9 +1,15 @@ import React from 'react'; -import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; -import Breadcrumbs from './Breadcrumbs'; -describe('', () => { +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; + +import ScreenHeader from './ScreenHeader'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + +describe('', () => { let breadcrumbWrapper; let breadcrumb; let breadcrumbItem; @@ -17,15 +23,15 @@ describe('', () => { }; const findChildren = () => { - breadcrumb = breadcrumbWrapper.find('Breadcrumb'); + breadcrumb = breadcrumbWrapper.find('ScreenHeader'); breadcrumbItem = breadcrumbWrapper.find('BreadcrumbItem'); - breadcrumbHeading = breadcrumbWrapper.find('BreadcrumbHeading'); + breadcrumbHeading = breadcrumbWrapper.find('Title'); }; test('initially renders succesfully', () => { - breadcrumbWrapper = mount( + breadcrumbWrapper = mountWithContexts( - + ); @@ -51,9 +57,9 @@ describe('', () => { ]; routes.forEach(([location, crumbLength]) => { - breadcrumbWrapper = mount( + breadcrumbWrapper = mountWithContexts( - + ); diff --git a/awx/ui_next/src/components/ScreenHeader/index.js b/awx/ui_next/src/components/ScreenHeader/index.js new file mode 100644 index 0000000000..7f5ab32733 --- /dev/null +++ b/awx/ui_next/src/components/ScreenHeader/index.js @@ -0,0 +1 @@ +export { default } from './ScreenHeader'; diff --git a/awx/ui_next/src/screens/Application/Applications.jsx b/awx/ui_next/src/screens/Application/Applications.jsx index 85995c8512..ae6fa94af1 100644 --- a/awx/ui_next/src/screens/Application/Applications.jsx +++ b/awx/ui_next/src/screens/Application/Applications.jsx @@ -12,7 +12,7 @@ import { import ApplicationsList from './ApplicationsList'; import ApplicationAdd from './ApplicationAdd'; import Application from './Application'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import { Detail, DetailList } from '../../components/DetailList'; const ApplicationAlert = styled(Alert)` @@ -45,7 +45,10 @@ function Applications({ i18n }) { return ( <> - + ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let wrapper; diff --git a/awx/ui_next/src/screens/Credential/Credentials.jsx b/awx/ui_next/src/screens/Credential/Credentials.jsx index beca652fd5..d883aa0dca 100644 --- a/awx/ui_next/src/screens/Credential/Credentials.jsx +++ b/awx/ui_next/src/screens/Credential/Credentials.jsx @@ -3,7 +3,7 @@ import { Route, Switch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import Credential from './Credential'; import CredentialAdd from './CredentialAdd'; import { CredentialList } from './CredentialList'; @@ -34,7 +34,10 @@ function Credentials({ i18n }) { return ( <> - + {({ me }) => } diff --git a/awx/ui_next/src/screens/Credential/Credentials.test.jsx b/awx/ui_next/src/screens/Credential/Credentials.test.jsx index f63c9d9b65..e5cb618fb2 100644 --- a/awx/ui_next/src/screens/Credential/Credentials.test.jsx +++ b/awx/ui_next/src/screens/Credential/Credentials.test.jsx @@ -3,6 +3,10 @@ import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Credentials from './Credentials'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let wrapper; @@ -30,8 +34,8 @@ describe('', () => { }, }); - expect(wrapper.find('Crumb').length).toBe(1); - expect(wrapper.find('BreadcrumbHeading').text()).toBe('Credentials'); + expect(wrapper.find('Crumb').length).toBe(0); + expect(wrapper.find('Title').text()).toBe('Credentials'); }); test('should display create new credential breadcrumb heading', () => { @@ -51,8 +55,6 @@ describe('', () => { }); expect(wrapper.find('Crumb').length).toBe(2); - expect(wrapper.find('BreadcrumbHeading').text()).toBe( - 'Create New Credential' - ); + expect(wrapper.find('Title').text()).toBe('Create New Credential'); }); }); diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypes.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypes.jsx index 7cdbbccb00..4eb47c1892 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypes.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypes.jsx @@ -6,7 +6,7 @@ import { Route, Switch } from 'react-router-dom'; import CredentialTypeAdd from './CredentialTypeAdd'; import CredentialTypeList from './CredentialTypeList'; import CredentialType from './CredentialType'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; function CredentialTypes({ i18n }) { const [breadcrumbConfig, setBreadcrumbConfig] = useState({ @@ -33,7 +33,10 @@ function CredentialTypes({ i18n }) { ); return ( <> - + diff --git a/awx/ui_next/src/screens/CredentialType/CredentialTypes.test.jsx b/awx/ui_next/src/screens/CredentialType/CredentialTypes.test.jsx index 468a55ac7a..bd8eff8e21 100644 --- a/awx/ui_next/src/screens/CredentialType/CredentialTypes.test.jsx +++ b/awx/ui_next/src/screens/CredentialType/CredentialTypes.test.jsx @@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import CredentialTypes from './CredentialTypes'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; let pageSections; diff --git a/awx/ui_next/src/screens/Dashboard/Dashboard.jsx b/awx/ui_next/src/screens/Dashboard/Dashboard.jsx index f60e049631..714e6cf152 100644 --- a/awx/ui_next/src/screens/Dashboard/Dashboard.jsx +++ b/awx/ui_next/src/screens/Dashboard/Dashboard.jsx @@ -18,7 +18,7 @@ import { import useRequest from '../../util/useRequest'; import { DashboardAPI } from '../../api'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import JobList from '../../components/JobList'; import ContentLoading from '../../components/ContentLoading'; import LineChart from './shared/LineChart'; @@ -117,7 +117,10 @@ function Dashboard({ i18n }) { } return ( - + ({ + ...jest.requireActual('react-router-dom'), +})); describe('', () => { let pageWrapper; diff --git a/awx/ui_next/src/screens/Host/Hosts.jsx b/awx/ui_next/src/screens/Host/Hosts.jsx index f66a28aa94..4c128bc202 100644 --- a/awx/ui_next/src/screens/Host/Hosts.jsx +++ b/awx/ui_next/src/screens/Host/Hosts.jsx @@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import HostList from './HostList'; import HostAdd from './HostAdd'; @@ -37,7 +37,7 @@ function Hosts({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/Host/Hosts.test.jsx b/awx/ui_next/src/screens/Host/Hosts.test.jsx index ba199f842f..1c0b9821c0 100644 --- a/awx/ui_next/src/screens/Host/Hosts.test.jsx +++ b/awx/ui_next/src/screens/Host/Hosts.test.jsx @@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Hosts from './Hosts'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { test('initially renders succesfully', () => { mountWithContexts(); @@ -27,7 +31,7 @@ describe('', () => { }, }, }); - expect(wrapper.find('BreadcrumbHeading').length).toBe(1); + expect(wrapper.find('Title').length).toBe(1); wrapper.unmount(); }); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx index 4fbdd5d9b2..a133bd91f2 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.jsx @@ -9,7 +9,7 @@ import InstanceGroup from './InstanceGroup'; import ContainerGroupAdd from './ContainerGroupAdd'; import ContainerGroup from './ContainerGroup'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; function InstanceGroups({ i18n }) { const [breadcrumbConfig, setBreadcrumbConfig] = useState({ @@ -54,7 +54,10 @@ function InstanceGroups({ i18n }) { ); return ( <> - + diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.test.jsx index 321b6ca71b..db32e7e4eb 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.test.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroups.test.jsx @@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import InstanceGroups from './InstanceGroups'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; let pageSections; diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index cf286c05eb..947a9be42a 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -4,7 +4,7 @@ import { t } from '@lingui/macro'; import { Route, Switch } from 'react-router-dom'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import { InventoryList } from './InventoryList'; import Inventory from './Inventory'; import SmartInventory from './SmartInventory'; @@ -95,7 +95,10 @@ function Inventories({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/Inventory/Inventories.test.jsx b/awx/ui_next/src/screens/Inventory/Inventories.test.jsx index ca452f877a..35e1ee5da0 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.test.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.test.jsx @@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Inventories from './Inventories'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; diff --git a/awx/ui_next/src/screens/Job/Job.test.jsx b/awx/ui_next/src/screens/Job/Job.test.jsx index 7b902984f0..afb1f6e148 100644 --- a/awx/ui_next/src/screens/Job/Job.test.jsx +++ b/awx/ui_next/src/screens/Job/Job.test.jsx @@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Job from './Jobs'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { test('initially renders succesfully', () => { mountWithContexts(); diff --git a/awx/ui_next/src/screens/Job/Jobs.jsx b/awx/ui_next/src/screens/Job/Jobs.jsx index 83c8d0d11f..318729407a 100644 --- a/awx/ui_next/src/screens/Job/Jobs.jsx +++ b/awx/ui_next/src/screens/Job/Jobs.jsx @@ -3,7 +3,7 @@ import { Route, Switch, useParams, useRouteMatch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { PageSection } from '@patternfly/react-core'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import Job from './Job'; import JobTypeRedirect from './JobTypeRedirect'; import JobList from '../../components/JobList'; @@ -40,7 +40,7 @@ function Jobs({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/Job/Jobs.test.jsx b/awx/ui_next/src/screens/Job/Jobs.test.jsx index 5782335404..e866a6d20d 100644 --- a/awx/ui_next/src/screens/Job/Jobs.test.jsx +++ b/awx/ui_next/src/screens/Job/Jobs.test.jsx @@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Jobs from './Jobs'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { test('initially renders succesfully', () => { mountWithContexts(); @@ -27,7 +31,7 @@ describe('', () => { }, }, }); - expect(wrapper.find('BreadcrumbHeading').length).toBe(1); + expect(wrapper.find('Title').length).toBe(1); wrapper.unmount(); }); }); diff --git a/awx/ui_next/src/screens/ManagementJob/ManagementJobs.jsx b/awx/ui_next/src/screens/ManagementJob/ManagementJobs.jsx index 774eb3e235..94f5a077c5 100644 --- a/awx/ui_next/src/screens/ManagementJob/ManagementJobs.jsx +++ b/awx/ui_next/src/screens/ManagementJob/ManagementJobs.jsx @@ -2,12 +2,13 @@ import React, { Fragment } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; function ManagementJobs({ i18n }) { return ( - diff --git a/awx/ui_next/src/screens/ManagementJob/ManagementJobs.test.jsx b/awx/ui_next/src/screens/ManagementJob/ManagementJobs.test.jsx index a667a47690..df422fe8ec 100644 --- a/awx/ui_next/src/screens/ManagementJob/ManagementJobs.test.jsx +++ b/awx/ui_next/src/screens/ManagementJob/ManagementJobs.test.jsx @@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import ManagementJobs from './ManagementJobs'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; @@ -17,6 +21,6 @@ describe('', () => { test('initially renders without crashing', () => { expect(pageWrapper.length).toBe(1); - expect(pageWrapper.find('Breadcrumbs').length).toBe(1); + expect(pageWrapper.find('ScreenHeader').length).toBe(1); }); }); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx index 2ae913202f..9d41166d1d 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx @@ -5,7 +5,7 @@ import { t } from '@lingui/macro'; import NotificationTemplateList from './NotificationTemplateList'; import NotificationTemplateAdd from './NotificationTemplateAdd'; import NotificationTemplate from './NotificationTemplate'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; function NotificationTemplates({ i18n }) { const match = useRouteMatch(); @@ -32,7 +32,10 @@ function NotificationTemplates({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx index 9333850cf9..f8b02d4735 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx @@ -2,6 +2,10 @@ import React from 'react'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import NotificationTemplates from './NotificationTemplates'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; let pageSections; diff --git a/awx/ui_next/src/screens/Organization/Organizations.jsx b/awx/ui_next/src/screens/Organization/Organizations.jsx index 5942d75147..6c7b17dc69 100644 --- a/awx/ui_next/src/screens/Organization/Organizations.jsx +++ b/awx/ui_next/src/screens/Organization/Organizations.jsx @@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import OrganizationsList from './OrganizationList/OrganizationList'; import OrganizationAdd from './OrganizationAdd/OrganizationAdd'; @@ -42,7 +42,10 @@ function Organizations({ i18n }) { return ( - + diff --git a/awx/ui_next/src/screens/Organization/Organizations.test.jsx b/awx/ui_next/src/screens/Organization/Organizations.test.jsx index 4f510463a4..819b86d88d 100644 --- a/awx/ui_next/src/screens/Organization/Organizations.test.jsx +++ b/awx/ui_next/src/screens/Organization/Organizations.test.jsx @@ -5,6 +5,9 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Organizations from './Organizations'; jest.mock('../../api'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); describe('', () => { test('initially renders succesfully', async () => { diff --git a/awx/ui_next/src/screens/Project/Projects.jsx b/awx/ui_next/src/screens/Project/Projects.jsx index a45dd1890b..8063d4c1e6 100644 --- a/awx/ui_next/src/screens/Project/Projects.jsx +++ b/awx/ui_next/src/screens/Project/Projects.jsx @@ -3,7 +3,7 @@ import { Route, withRouter, Switch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import ProjectsList from './ProjectList/ProjectList'; import ProjectAdd from './ProjectAdd/ProjectAdd'; @@ -45,7 +45,7 @@ function Projects({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/Project/Projects.test.jsx b/awx/ui_next/src/screens/Project/Projects.test.jsx index b46f37ae23..4d522a3d14 100644 --- a/awx/ui_next/src/screens/Project/Projects.test.jsx +++ b/awx/ui_next/src/screens/Project/Projects.test.jsx @@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Projects from './Projects'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { test('initially renders succesfully', () => { mountWithContexts(); @@ -27,7 +31,7 @@ describe('', () => { }, }, }); - expect(wrapper.find('BreadcrumbHeading').length).toBe(1); + expect(wrapper.find('Title').length).toBe(1); wrapper.unmount(); }); }); diff --git a/awx/ui_next/src/screens/Schedule/AllSchedules.jsx b/awx/ui_next/src/screens/Schedule/AllSchedules.jsx index a55e9df3bb..4e778e9623 100644 --- a/awx/ui_next/src/screens/Schedule/AllSchedules.jsx +++ b/awx/ui_next/src/screens/Schedule/AllSchedules.jsx @@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { PageSection, Card } from '@patternfly/react-core'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import { ScheduleList } from '../../components/Schedule'; import { SchedulesAPI } from '../../api'; @@ -19,7 +19,8 @@ function AllSchedules({ i18n }) { return ( <> - ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let wrapper; @@ -30,7 +34,6 @@ describe('', () => { }, }); - expect(wrapper.find('Crumb').length).toBe(1); - expect(wrapper.find('BreadcrumbHeading').text()).toBe('Schedules'); + expect(wrapper.find('Title').text()).toBe('Schedules'); }); }); diff --git a/awx/ui_next/src/screens/Setting/Settings.jsx b/awx/ui_next/src/screens/Setting/Settings.jsx index ae3356f950..a535384afa 100644 --- a/awx/ui_next/src/screens/Setting/Settings.jsx +++ b/awx/ui_next/src/screens/Setting/Settings.jsx @@ -5,7 +5,7 @@ import { t } from '@lingui/macro'; import { PageSection, Card } from '@patternfly/react-core'; import ContentError from '../../components/ContentError'; import ContentLoading from '../../components/ContentLoading'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import ActivityStream from './ActivityStream'; import AzureAD from './AzureAD'; import GitHub from './GitHub'; @@ -129,7 +129,7 @@ function Settings({ i18n }) { return ( - + diff --git a/awx/ui_next/src/screens/Setting/Settings.test.jsx b/awx/ui_next/src/screens/Setting/Settings.test.jsx index 24a82986f8..3c63750e71 100644 --- a/awx/ui_next/src/screens/Setting/Settings.test.jsx +++ b/awx/ui_next/src/screens/Setting/Settings.test.jsx @@ -13,6 +13,9 @@ jest.mock('../../api/models/Settings'); SettingsAPI.readAllOptions.mockResolvedValue({ data: mockAllOptions, }); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); describe('', () => { let wrapper; diff --git a/awx/ui_next/src/screens/Team/Teams.jsx b/awx/ui_next/src/screens/Team/Teams.jsx index 3022ca98c7..f71098752a 100644 --- a/awx/ui_next/src/screens/Team/Teams.jsx +++ b/awx/ui_next/src/screens/Team/Teams.jsx @@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Config } from '../../contexts/Config'; -import Breadcrumbs from '../../components/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader'; import TeamList from './TeamList'; import TeamAdd from './TeamAdd'; import Team from './Team'; @@ -36,7 +36,7 @@ function Teams({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/Team/Teams.test.jsx b/awx/ui_next/src/screens/Team/Teams.test.jsx index db73b4d7d4..181b814077 100644 --- a/awx/ui_next/src/screens/Team/Teams.test.jsx +++ b/awx/ui_next/src/screens/Team/Teams.test.jsx @@ -3,6 +3,9 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Teams from './Teams'; jest.mock('../../api'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); describe('', () => { test('initially renders succesfully', () => { diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx index 167d458141..5f471baaa5 100644 --- a/awx/ui_next/src/screens/Template/Templates.jsx +++ b/awx/ui_next/src/screens/Template/Templates.jsx @@ -4,7 +4,7 @@ import { t } from '@lingui/macro'; import { Route, withRouter, Switch } from 'react-router-dom'; import { PageSection } from '@patternfly/react-core'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import { TemplateList } from './TemplateList'; import Template from './Template'; import WorkflowJobTemplate from './WorkflowJobTemplate'; @@ -12,22 +12,24 @@ import JobTemplateAdd from './JobTemplateAdd'; import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd'; function Templates({ i18n }) { - const initBreadcrumbs = useRef({ + const initScreenHeader = useRef({ '/templates': i18n._(t`Templates`), '/templates/job_template/add': i18n._(t`Create New Job Template`), '/templates/workflow_job_template/add': i18n._( t`Create New Workflow Template` ), }); - const [breadcrumbConfig, setBreadcrumbs] = useState(initBreadcrumbs.current); + const [breadcrumbConfig, setScreenHeader] = useState( + initScreenHeader.current + ); const setBreadcrumbConfig = useCallback( (template, schedule) => { if (!template) return; const templatePath = `/templates/${template.type}/${template.id}`; const schedulesPath = `${templatePath}/schedules`; const surveyPath = `${templatePath}/survey`; - setBreadcrumbs({ - ...initBreadcrumbs.current, + setScreenHeader({ + ...initScreenHeader.current, [templatePath]: `${template.name}`, [`${templatePath}/details`]: i18n._(t`Details`), [`${templatePath}/edit`]: i18n._(t`Edit Details`), @@ -49,7 +51,10 @@ function Templates({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/Template/Templates.test.jsx b/awx/ui_next/src/screens/Template/Templates.test.jsx index f39643053d..1b126cda31 100644 --- a/awx/ui_next/src/screens/Template/Templates.test.jsx +++ b/awx/ui_next/src/screens/Template/Templates.test.jsx @@ -3,6 +3,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Templates from './Templates'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { let pageWrapper; diff --git a/awx/ui_next/src/screens/User/Users.jsx b/awx/ui_next/src/screens/User/Users.jsx index 35e4c4517b..323e8e37a4 100644 --- a/awx/ui_next/src/screens/User/Users.jsx +++ b/awx/ui_next/src/screens/User/Users.jsx @@ -3,7 +3,7 @@ import { Route, useRouteMatch, Switch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; import { Config } from '../../contexts/Config'; import UsersList from './UserList/UserList'; @@ -46,7 +46,7 @@ function Users({ i18n }) { ); return ( - + diff --git a/awx/ui_next/src/screens/User/Users.test.jsx b/awx/ui_next/src/screens/User/Users.test.jsx index 7193234f8f..862934e99b 100644 --- a/awx/ui_next/src/screens/User/Users.test.jsx +++ b/awx/ui_next/src/screens/User/Users.test.jsx @@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import Users from './Users'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { test('initially renders successfully', () => { mountWithContexts(); @@ -27,7 +31,7 @@ describe('', () => { }, }, }); - expect(wrapper.find('BreadcrumbHeading').length).toBe(1); + expect(wrapper.find('Title').length).toBe(1); wrapper.unmount(); }); }); diff --git a/awx/ui_next/src/screens/WorkflowApproval/WorkflowApprovals.jsx b/awx/ui_next/src/screens/WorkflowApproval/WorkflowApprovals.jsx index a8d66ccdba..84654810fa 100644 --- a/awx/ui_next/src/screens/WorkflowApproval/WorkflowApprovals.jsx +++ b/awx/ui_next/src/screens/WorkflowApproval/WorkflowApprovals.jsx @@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import WorkflowApprovalList from './WorkflowApprovalList'; import WorkflowApproval from './WorkflowApproval'; -import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; +import ScreenHeader from '../../components/ScreenHeader/ScreenHeader'; function WorkflowApprovals({ i18n }) { const match = useRouteMatch(); @@ -26,7 +26,10 @@ function WorkflowApprovals({ i18n }) { return ( <> - + diff --git a/awx/ui_next/src/screens/WorkflowApproval/WorkflowApprovals.test.jsx b/awx/ui_next/src/screens/WorkflowApproval/WorkflowApprovals.test.jsx index b5bfdcf2a0..45aaffa7bf 100644 --- a/awx/ui_next/src/screens/WorkflowApproval/WorkflowApprovals.test.jsx +++ b/awx/ui_next/src/screens/WorkflowApproval/WorkflowApprovals.test.jsx @@ -3,6 +3,10 @@ import { createMemoryHistory } from 'history'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import WorkflowApprovals from './WorkflowApprovals'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + describe('', () => { test('initially renders succesfully', () => { mountWithContexts(); @@ -29,7 +33,8 @@ describe('', () => { }, }, }); - expect(wrapper.find('BreadcrumbHeading').length).toBe(1); + + expect(wrapper.find('Title').length).toBe(1); wrapper.unmount(); }); }); From 87a2039ded4d82ab9bfc75a20c3f0733a7aadc73 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 14 Jan 2021 09:50:01 -0500 Subject: [PATCH 02/17] don't strip out non-namespaced params when encoding url search params --- .../src/components/ListHeader/ListHeader.jsx | 7 ++++++- .../PaginatedDataList/PaginatedDataList.jsx | 11 +++++++++-- .../src/components/PaginatedTable/HeaderRow.jsx | 7 ++++++- .../components/PaginatedTable/PaginatedTable.jsx | 9 +++++++-- awx/ui_next/src/util/qs.js | 15 ++++++++++----- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.jsx index 6e1f9cb4e9..d5e359c3bc 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.jsx @@ -85,7 +85,12 @@ class ListHeader extends React.Component { pushHistoryState(params) { const { history, qsConfig } = this.props; const { pathname } = history.location; - const encodedParams = encodeNonDefaultQueryString(qsConfig, params); + const nonNamespacedParams = parseQueryString({}, history.location.search); + const encodedParams = encodeNonDefaultQueryString( + qsConfig, + params, + nonNamespacedParams + ); history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); } diff --git a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx index 7f5fe9afdd..a31553658d 100644 --- a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx @@ -60,8 +60,15 @@ function PaginatedDataList({ pushHistoryState(replaceParams(oldParams, { page_size: pageSize, page })); }; - const pushHistoryState = params => { - const encodedParams = encodeNonDefaultQueryString(qsConfig, params); + const pushHistoryState = (params) => { + const { history, qsConfig } = this.props; + const { pathname } = history.location; + const nonNamespacedParams = parseQueryString({}, history.location.search); + const encodedParams = encodeNonDefaultQueryString( + qsConfig, + params, + nonNamespacedParams + ); history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); }; diff --git a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx index 259cc39bac..14c5c0b8ee 100644 --- a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx +++ b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx @@ -23,7 +23,12 @@ export default function HeaderRow({ qsConfig, children }) { order_by: order === 'asc' ? key : `-${key}`, page: null, }); - const encodedParams = encodeNonDefaultQueryString(qsConfig, newParams); + const nonNamespacedParams = parseQueryString({}, history.location.search); + const encodedParams = encodeNonDefaultQueryString( + qsConfig, + newParams, + nonNamespacedParams + ); history.push( encodedParams ? `${location.pathname}?${encodedParams}` diff --git a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx index 9892df34fe..42bf01a638 100644 --- a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx +++ b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx @@ -40,8 +40,13 @@ function PaginatedTable({ const history = useHistory(); const pushHistoryState = params => { - const { pathname } = history.location; - const encodedParams = encodeNonDefaultQueryString(qsConfig, params); + const { pathname, search } = history.location; + const nonNamespacedParams = parseQueryString({}, search); + const encodedParams = encodeNonDefaultQueryString( + qsConfig, + params, + nonNamespacedParams + ); history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); }; diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index c02e3be075..729a28790d 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -118,15 +118,20 @@ function encodeValue(key, value) { * removing defaults. Used to put into url bar after ui route * @param {object} qs config object for namespacing params, filtering defaults * @param {object} query param object + * @param {object} any non-namespaced params to append * @return {string} url query string */ -export const encodeNonDefaultQueryString = (config, params) => { +export const encodeNonDefaultQueryString = ( + config, + params, + nonNamespacedParams = {} +) => { if (!params) return ''; - const paramsWithoutDefaults = removeParams({}, params, config.defaultParams); - return encodeQueryString( - namespaceParams(config.namespace, paramsWithoutDefaults) - ); + return encodeQueryString({ + ...namespaceParams(config.namespace, paramsWithoutDefaults), + ...nonNamespacedParams, + }); }; /** From 7c57a8e5d092369f64a1a1f195457e214fba43f9 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 14 Jan 2021 09:50:17 -0500 Subject: [PATCH 03/17] add activity stream ui --- awx/ui_next/src/api/index.js | 3 + awx/ui_next/src/api/models/ActivityStream.js | 10 + awx/ui_next/src/routeConfig.js | 6 + .../screens/ActivityStream/ActivityStream.jsx | 228 +++++++ .../ActivityStream/ActivityStream.test.jsx | 25 + .../ActivityStream/ActivityStreamListItem.jsx | 61 ++ .../ActivityStreamListItem.test.jsx | 22 + .../ActivityStream/StreamDetailButton.jsx | 64 ++ .../StreamDetailButton.test.jsx | 21 + .../buildActivityDescription.jsx | 578 ++++++++++++++++++ .../buildActivityDescription.test.jsx | 10 + .../src/screens/ActivityStream/index.js | 1 + 12 files changed, 1029 insertions(+) create mode 100644 awx/ui_next/src/api/models/ActivityStream.js create mode 100644 awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx create mode 100644 awx/ui_next/src/screens/ActivityStream/ActivityStream.test.jsx create mode 100644 awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.jsx create mode 100644 awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.test.jsx create mode 100644 awx/ui_next/src/screens/ActivityStream/StreamDetailButton.jsx create mode 100644 awx/ui_next/src/screens/ActivityStream/StreamDetailButton.test.jsx create mode 100644 awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx create mode 100644 awx/ui_next/src/screens/ActivityStream/buildActivityDescription.test.jsx create mode 100644 awx/ui_next/src/screens/ActivityStream/index.js diff --git a/awx/ui_next/src/api/index.js b/awx/ui_next/src/api/index.js index f5f0c05330..cddf01e259 100644 --- a/awx/ui_next/src/api/index.js +++ b/awx/ui_next/src/api/index.js @@ -1,3 +1,4 @@ +import ActivityStream from './models/ActivityStream'; import AdHocCommands from './models/AdHocCommands'; import Applications from './models/Applications'; import Auth from './models/Auth'; @@ -39,6 +40,7 @@ import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes'; import WorkflowJobTemplates from './models/WorkflowJobTemplates'; import WorkflowJobs from './models/WorkflowJobs'; +const ActivityStreamAPI = new ActivityStream(); const AdHocCommandsAPI = new AdHocCommands(); const ApplicationsAPI = new Applications(); const AuthAPI = new Auth(); @@ -81,6 +83,7 @@ const WorkflowJobTemplatesAPI = new WorkflowJobTemplates(); const WorkflowJobsAPI = new WorkflowJobs(); export { + ActivityStreamAPI, AdHocCommandsAPI, ApplicationsAPI, AuthAPI, diff --git a/awx/ui_next/src/api/models/ActivityStream.js b/awx/ui_next/src/api/models/ActivityStream.js new file mode 100644 index 0000000000..99b65bc634 --- /dev/null +++ b/awx/ui_next/src/api/models/ActivityStream.js @@ -0,0 +1,10 @@ +import Base from '../Base'; + +class ActivityStream extends Base { + constructor(http) { + super(http); + this.baseUrl = '/api/v2/activity_stream/'; + } +} + +export default ActivityStream; diff --git a/awx/ui_next/src/routeConfig.js b/awx/ui_next/src/routeConfig.js index 0b9e0591fa..a343a7d1e0 100644 --- a/awx/ui_next/src/routeConfig.js +++ b/awx/ui_next/src/routeConfig.js @@ -1,5 +1,6 @@ import { t } from '@lingui/macro'; +import ActivityStream from './screens/ActivityStream'; import Applications from './screens/Application'; import Credentials from './screens/Credential'; import CredentialTypes from './screens/CredentialType'; @@ -44,6 +45,11 @@ function getRouteConfig(i18n) { path: '/schedules', screen: Schedules, }, + { + title: i18n._(t`Activity Stream`), + path: '/activity_stream', + screen: ActivityStream, + }, { title: i18n._(t`Workflow Approvals`), path: '/workflow_approvals', diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx new file mode 100644 index 0000000000..86b715ebc1 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -0,0 +1,228 @@ +import React, { Fragment, useState, useEffect, useCallback } from 'react'; +import { useLocation, useHistory } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + Card, + PageSection, + PageSectionVariants, + Select, + SelectVariant, + SelectOption, + Title, +} from '@patternfly/react-core'; + +import DatalistToolbar from '../../components/DataListToolbar'; +import PaginatedTable, { + HeaderRow, + HeaderCell, +} from '../../components/PaginatedTable'; +import useRequest from '../../util/useRequest'; +import { getQSConfig, parseQueryString } from '../../util/qs'; +import { ActivityStreamAPI } from '../../api'; + +import ActivityStreamListItem from './ActivityStreamListItem'; + +function ActivityStream({ i18n }) { + const { light } = PageSectionVariants; + + const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false); + const location = useLocation(); + const history = useHistory(); + const urlParams = new URLSearchParams(location.search); + + const activityStreamType = urlParams.get('type'); + + let typeParams = {}; + + if (activityStreamType !== 'all') { + typeParams = { + or__object1__in: activityStreamType, + or__object2__in: activityStreamType, + }; + } + + const QS_CONFIG = getQSConfig( + 'activity_stream', + { + page: 1, + page_size: 20, + order_by: '-timestamp', + }, + ['id', 'page', 'page_size'] + ); + + const { + result: { results, count, relatedSearchableKeys, searchableKeys }, + error: contentError, + isLoading, + request: fetchActivityStream, + } = useRequest( + useCallback( + async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const [response, actionsResponse] = await Promise.all([ + ActivityStreamAPI.read({ ...params, ...typeParams }), + ActivityStreamAPI.readOptions(), + ]); + return { + results: response.data.results, + count: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + }; + }, + [location] // eslint-disable-line react-hooks/exhaustive-deps + ), + { + results: [], + count: 0, + relatedSearchableKeys: [], + searchableKeys: [], + } + ); + useEffect(() => { + fetchActivityStream(); + }, [fetchActivityStream]); + + return ( + + + + {i18n._(t`Activity Stream`)} + + + + + + + {i18n._(t`Time`)} + {i18n._(t`Initiated by`)} + {i18n._(t`Event`)} + {i18n._(t`Actions`)} + + } + renderToolbar={props => ( + + )} + renderRow={streamItem => ( + + )} + /> + + + + ); +} + +export default withI18n()(ActivityStream); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.test.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.test.jsx new file mode 100644 index 0000000000..b5afeef3d6 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.test.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; + +import ActivityStream from './ActivityStream'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), +})); + +describe('', () => { + let pageWrapper; + + beforeEach(() => { + pageWrapper = mountWithContexts(); + }); + + afterEach(() => { + pageWrapper.unmount(); + }); + + test('initially renders without crashing', () => { + expect(pageWrapper.length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.jsx new file mode 100644 index 0000000000..ab961aeb06 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { shape } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { Tr, Td } from '@patternfly/react-table'; +import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; + +import { formatDateString } from '../../util/dates'; +import { ActionsTd, ActionItem } from '../../components/PaginatedTable'; + +import StreamDetailButton from './StreamDetailButton'; +import buildDescription from './buildActivityDescription'; + +function ActivityStreamListItem({ streamItem, i18n }) { + ActivityStreamListItem.propTypes = { + streamItem: shape({}).isRequired, + }; + + const buildUser = item => { + let link; + if (item?.summary_fields?.actor?.id) { + link = ( + + {item.summary_fields.actor.username} + + ); + } else if (item?.summary_fields?.actor) { + link = i18n._(t`${item.summary_fields.actor.username} (deleted)`); + } else { + link = i18n._(t`system`); + } + return link; + }; + + const labelId = `check-action-${streamItem.id}`; + const user = buildUser(streamItem); + const description = buildDescription(streamItem, i18n); + + return ( + + + + {streamItem.timestamp ? formatDateString(streamItem.timestamp) : ''} + + {user} + + {description} + + + + + + + + ); +} +export default withI18n()(ActivityStreamListItem); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.test.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.test.jsx new file mode 100644 index 0000000000..c0f03d23ee --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamListItem.test.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import ActivityStreamListItem from './ActivityStreamListItem'; + +jest.mock('../../api/models/ActivityStream'); + +describe('', () => { + test('initially renders succesfully', () => { + mountWithContexts( + + + {}} + /> + +
+ ); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.jsx b/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.jsx new file mode 100644 index 0000000000..b2e8dc368b --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.jsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Button, Modal } from '@patternfly/react-core'; +import { SearchPlusIcon } from '@patternfly/react-icons'; + +import { formatDateString } from '../../util/dates'; + +import { DetailList, Detail } from '../../components/DetailList'; +import { VariablesDetail } from '../../components/CodeMirrorInput'; + +function StreamDetailButton({ i18n, streamItem, user, description }) { + const [isOpen, setIsOpen] = useState(false); + + const setting = streamItem?.summary_fields?.setting; + const changeRows = Math.max( + Object.keys(streamItem?.changes || []).length + 2, + 6 + ); + + return ( + <> + + setIsOpen(false)} + > + + + + + + + + + + + ); +} + +export default withI18n()(StreamDetailButton); diff --git a/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.test.jsx b/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.test.jsx new file mode 100644 index 0000000000..04c112c9e0 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.test.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import StreamDetailButton from './StreamDetailButton'; + +jest.mock('../../api/models/ActivityStream'); + +describe('', () => { + test('initially renders succesfully', () => { + mountWithContexts( + Bob} + description={foo} + /> + ); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx b/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx new file mode 100644 index 0000000000..6e9f12b9c9 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx @@ -0,0 +1,578 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { t } from '@lingui/macro'; + +const buildAnchor = (obj, resource, activity) => { + let url; + let name; + // try/except pattern asserts that: + // if we encounter a case where a UI url can't or + // shouldn't be generated, just supply the name of the resource + try { + // catch-all case to avoid generating urls if a resource has been deleted + // if a resource still exists, it'll be serialized in the activity's summary_fields + if (!activity.summary_fields[resource]) { + throw new Error('The referenced resource no longer exists'); + } + switch (resource) { + case 'custom_inventory_script': + url = `/inventory_scripts/${obj.id}/`; + break; + case 'group': + if ( + activity.operation === 'create' || + activity.operation === 'delete' + ) { + // the API formats the changes.inventory field as str 'myInventoryName-PrimaryKey' + const [inventory_id] = activity.changes.inventory + .split('-') + .slice(-1); + url = `/inventories/inventory/${inventory_id}/groups/${activity.changes.id}/details/`; + } else { + url = `/inventories/inventory/${ + activity.summary_fields.inventory[0].id + }/groups/${activity.changes.id || + activity.changes.object1_pk}/details/`; + } + break; + case 'host': + url = `/hosts/${obj.id}/`; + break; + case 'job': + url = `/jobs/${obj.id}/`; + break; + case 'inventory': + url = + obj?.kind === 'smart' + ? `/inventories/smart_inventory/${obj.id}/` + : `/inventories/inventory/${obj.id}/`; + break; + case 'schedule': + // schedule urls depend on the resource they're associated with + if (activity.summary_fields.job_template) { + const jt_id = activity.summary_fields.job_template[0].id; + url = `/templates/job_template/${jt_id}/schedules/${obj.id}/`; + } else if (activity.summary_fields.project) { + url = `/projects/${activity.summary_fields.project[0].id}/schedules/${obj.id}/`; + } else if (activity.summary_fields.system_job_template) { + url = null; + } else { + // urls for inventory sync schedules currently depend on having + // an inventory id and group id + throw new Error( + 'activity.summary_fields to build this url not implemented yet' + ); + } + break; + case 'setting': + url = `/settings/`; + break; + case 'notification_template': + url = `/notification_templates/${obj.id}/`; + break; + case 'role': + throw new Error( + 'role object management is not consolidated to a single UI view' + ); + case 'job_template': + url = `/templates/job_template/${obj.id}/`; + break; + case 'workflow_job_template': + url = `/templates/workflow_job_template/${obj.id}/`; + break; + case 'workflow_job_template_node': { + const { + wfjt_id, + wfjt_name, + } = activity.summary_fields.workflow_job_template[0]; + url = `/templates/workflow_job_template/${wfjt_id}/`; + name = wfjt_name; + break; + } + case 'workflow_job': + url = `/workflows/${obj.id}/`; + break; + case 'label': + url = null; + break; + case 'inventory_source': { + const inventoryId = (obj.inventory || '').split('-').reverse()[0]; + url = `/inventories/inventory/${inventoryId}/sources/${obj.id}/details/`; + break; + } + case 'o_auth2_application': + url = `/applications/${obj.id}/`; + break; + case 'workflow_approval': + url = `/jobs/workflow/${activity.summary_fields.workflow_job[0].id}/output/`; + name = `${activity.summary_fields.workflow_job[0].name} | ${activity.summary_fields.workflow_approval[0].name}`; + break; + case 'workflow_approval_template': + url = `/templates/workflow_job_template/${activity.summary_fields.workflow_job_template[0].id}/visualizer/`; + name = `${activity.summary_fields.workflow_job_template[0].name} | ${activity.summary_fields.workflow_approval_template[0].name}`; + break; + default: + url = `/${resource}s/${obj.id}/`; + } + + name = name || obj.name || obj.username; + + if (url) { + return {name}; + } + + return {name}; + } catch (err) { + return {obj.name || obj.username || ''}; + } +}; + +const getPastTense = item => { + return /e$/.test(item) ? `${item}d ` : `${item}ed `; +}; + +const isGroupRelationship = item => { + return ( + item.object1 === 'group' && + item.object2 === 'group' && + item.summary_fields.group.length > 1 + ); +}; + +const buildLabeledLink = (label, link) => { + return ( + + {label} {link} + + ); +}; + +export default (activity, i18n) => { + const labeledLinks = []; + // Activity stream objects will outlive the resources they reference + // in that case, summary_fields will not be available - show generic error text instead + try { + switch (activity.object_association) { + // explicit role dis+associations + case 'role': { + let { object1, object2 } = activity; + + // if object1 winds up being the role's resource, we need to swap the objects + // in order to make the sentence make sense. + if (activity.object_type === object1) { + object1 = activity.object2; + object2 = activity.object1; + } + + // object1 field is resource targeted by the dis+association + // object2 field is the resource the role is inherited from + // summary_field.role[0] contains ref info about the role + switch (activity.operation) { + // expected outcome: "disassociated role_name from " + case 'disassociate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields.group[1], + object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} from`, + buildAnchor( + activity.summary_fields.group[0], + object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields[object2][0], + object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} from`, + buildAnchor( + activity.summary_fields[object1][0], + object1, + activity + ) + ) + ); + } + break; + // expected outcome: "associated role_name to " + case 'associate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields.group[1], + object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} to `, + buildAnchor( + activity.summary_fields.group[0], + object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields[object2][0], + object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} to `, + buildAnchor( + activity.summary_fields[object1][0], + object1, + activity + ) + ) + ); + } + break; + default: + break; + } + break; + // inherited role dis+associations (logic identical to case 'role') + } + case 'parents': + // object1 field is resource targeted by the dis+association + // object2 field is the resource the role is inherited from + // summary_field.role[0] contains ref info about the role + switch (activity.operation) { + // expected outcome: "disassociated role_name from " + case 'disassociate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object2}`, + buildAnchor( + activity.summary_fields.group[1], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `from ${activity.object1}`, + buildAnchor( + activity.summary_fields.group[0], + activity.object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields[activity.object2][0], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} from`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } + break; + // expected outcome: "associated role_name to " + case 'associate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor( + activity.summary_fields.group[0], + activity.object1, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `to ${activity.object2}`, + buildAnchor( + activity.summary_fields.group[1], + activity.object2, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + getPastTense(activity.operation), + buildAnchor( + activity.summary_fields[activity.object2][0], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `${activity.summary_fields.role[0].role_field} to`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } + break; + default: + break; + } + break; + // CRUD operations / resource on resource dis+associations + default: + switch (activity.operation) { + // expected outcome: "disassociated from " + case 'disassociate': + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object2}`, + buildAnchor( + activity.summary_fields.group[1], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `from ${activity.object1}`, + buildAnchor( + activity.summary_fields.group[0], + activity.object1, + activity + ) + ) + ); + } else if ( + activity.object1 === 'workflow_job_template_node' && + activity.object2 === 'workflow_job_template_node' + ) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} two nodes on workflow`, + buildAnchor( + activity.summary_fields[activity.object1[0]], + activity.object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object2}`, + buildAnchor( + activity.summary_fields[activity.object2][0], + activity.object2, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `from ${activity.object1}`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } + break; + // expected outcome "associated to " + case 'associate': + // groups are the only resource that can be associated/disassociated into each other + if (isGroupRelationship(activity)) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor( + activity.summary_fields.group[0], + activity.object1, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `to ${activity.object2}`, + buildAnchor( + activity.summary_fields.group[1], + activity.object2, + activity + ) + ) + ); + } else if ( + activity.object1 === 'workflow_job_template_node' && + activity.object2 === 'workflow_job_template_node' + ) { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} two nodes on workflow`, + buildAnchor( + activity.summary_fields[activity.object1[0]], + activity.object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + labeledLinks.push( + buildLabeledLink( + `to ${activity.object2}`, + buildAnchor( + activity.summary_fields[activity.object2][0], + activity.object2, + activity + ) + ) + ); + } + break; + case 'delete': + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor(activity.changes, activity.object1, activity) + ) + ); + break; + // expected outcome: "operation " + case 'update': + if ( + activity.object1 === 'workflow_approval' && + activity?.changes?.status?.length === 2 + ) { + let operationText = ''; + if (activity.changes.status[1] === 'successful') { + operationText = i18n._(t`approved`); + } else if (activity.changes.status[1] === 'failed') { + if ( + activity.changes.timed_out && + activity.changes.timed_out[1] === true + ) { + operationText = i18n._(t`timed out`); + } else { + operationText = i18n._(t`denied`); + } + } else { + operationText = i18n._(t`updated`); + } + labeledLinks.push( + buildLabeledLink( + `${operationText} ${activity.object1}`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } else { + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor( + activity.summary_fields[activity.object1][0], + activity.object1, + activity + ) + ) + ); + } + break; + case 'create': + labeledLinks.push( + buildLabeledLink( + `${getPastTense(activity.operation)} ${activity.object1}`, + buildAnchor(activity.changes, activity.object1, activity) + ) + ); + break; + default: + break; + } + break; + } + } catch (err) { + return {i18n._(t`Event summary not available`)}; + } + + return ( + + {labeledLinks.reduce( + (acc, x) => + acc === null ? ( + x + ) : ( + <> + {acc} {x} + + ), + null + )} + + ); +}; diff --git a/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.test.jsx b/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.test.jsx new file mode 100644 index 0000000000..3d90ef5915 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.test.jsx @@ -0,0 +1,10 @@ +import { mount } from 'enzyme'; + +import buildDescription from './buildActivityDescription'; + +describe('buildActivityStream', () => { + test('initially renders succesfully', () => { + const description = mount(buildDescription({}, {})); + expect(description.find('span').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/index.js b/awx/ui_next/src/screens/ActivityStream/index.js new file mode 100644 index 0000000000..5c0c72d9ef --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/index.js @@ -0,0 +1 @@ +export { default } from './ActivityStream'; From cec5a77762733f76361025f968b695f231b82f9f Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 14 Jan 2021 11:16:34 -0500 Subject: [PATCH 04/17] add username-based search to activity stream --- awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx index 86b715ebc1..88527fc13f 100644 --- a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -195,6 +195,10 @@ function ActivityStream({ i18n }) { key: 'search', isDefault: true, }, + { + name: i18n._(t`Initiated by (username)`), + key: 'actor__username__icontains', + }, ]} toolbarSortColumns={[ { From 2f7a7b453f0bac0d606e674b0ee5b4c383c82610 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 14 Jan 2021 12:47:47 -0500 Subject: [PATCH 05/17] add workflow node based events to be shown when templates is selected in activity streram --- awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx | 2 +- awx/ui_next/src/screens/Template/Templates.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx index 88527fc13f..42621b3344 100644 --- a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -162,7 +162,7 @@ function ActivityStream({ i18n }) { {i18n._(t`Templates`)} diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx index 5f471baaa5..a3e819c151 100644 --- a/awx/ui_next/src/screens/Template/Templates.jsx +++ b/awx/ui_next/src/screens/Template/Templates.jsx @@ -52,7 +52,7 @@ function Templates({ i18n }) { return ( <> From fd708456df0c186bab5f7b0b223d6d7b44cc1e50 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 14 Jan 2021 12:48:20 -0500 Subject: [PATCH 06/17] fix workflow event activity steam linking --- .../screens/ActivityStream/buildActivityDescription.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx b/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx index 6e9f12b9c9..645923a9cb 100644 --- a/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx +++ b/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx @@ -52,6 +52,9 @@ const buildAnchor = (obj, resource, activity) => { if (activity.summary_fields.job_template) { const jt_id = activity.summary_fields.job_template[0].id; url = `/templates/job_template/${jt_id}/schedules/${obj.id}/`; + } else if (activity.summary_fields.workflow_job_template) { + const wfjt_id = activity.summary_fields.workflow_job_template[0].id; + url = `/templates/workflow_job_template/${wfjt_id}/schedules/${obj.id}/`; } else if (activity.summary_fields.project) { url = `/projects/${activity.summary_fields.project[0].id}/schedules/${obj.id}/`; } else if (activity.summary_fields.system_job_template) { @@ -82,8 +85,8 @@ const buildAnchor = (obj, resource, activity) => { break; case 'workflow_job_template_node': { const { - wfjt_id, - wfjt_name, + id: wfjt_id, + name: wfjt_name, } = activity.summary_fields.workflow_job_template[0]; url = `/templates/workflow_job_template/${wfjt_id}/`; name = wfjt_name; From 77cd875a9c4529261acbdddd551af46edcda7f2a Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 15 Jan 2021 12:52:49 -0500 Subject: [PATCH 07/17] add initiated by sort on activity stream --- awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx index 42621b3344..6d9227e8b5 100644 --- a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -205,13 +205,19 @@ function ActivityStream({ i18n }) { name: i18n._(t`Time`), key: 'timestamp', }, + { + name: i18n._(t`Initiated by`), + key: 'actor__username', + }, ]} toolbarSearchableKeys={searchableKeys} toolbarRelatedSearchableKeys={relatedSearchableKeys} headerRow={ {i18n._(t`Time`)} - {i18n._(t`Initiated by`)} + + {i18n._(t`Initiated by`)} + {i18n._(t`Event`)} {i18n._(t`Actions`)} From c793b3a9c84121605a4210f26a6c848d9f738e3f Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 15 Jan 2021 14:32:34 -0500 Subject: [PATCH 08/17] fix issues from rebase fallout --- .../src/components/PaginatedDataList/PaginatedDataList.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx index a31553658d..92b471c8c3 100644 --- a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx @@ -60,9 +60,7 @@ function PaginatedDataList({ pushHistoryState(replaceParams(oldParams, { page_size: pageSize, page })); }; - const pushHistoryState = (params) => { - const { history, qsConfig } = this.props; - const { pathname } = history.location; + const pushHistoryState = params => { const nonNamespacedParams = parseQueryString({}, history.location.search); const encodedParams = encodeNonDefaultQueryString( qsConfig, From ce28968a118d42069b325a887eab4eae2d532cc5 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 19 Jan 2021 10:06:08 -0500 Subject: [PATCH 09/17] reset page to 1 after activity stream type changes --- .../screens/ActivityStream/ActivityStream.jsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx index 6d9227e8b5..bd36e33821 100644 --- a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -18,7 +18,12 @@ import PaginatedTable, { HeaderCell, } from '../../components/PaginatedTable'; import useRequest from '../../util/useRequest'; -import { getQSConfig, parseQueryString } from '../../util/qs'; +import { + getQSConfig, + parseQueryString, + replaceParams, + encodeNonDefaultQueryString, +} from '../../util/qs'; import { ActivityStreamAPI } from '../../api'; import ActivityStreamListItem from './ActivityStreamListItem'; @@ -89,6 +94,19 @@ function ActivityStream({ i18n }) { fetchActivityStream(); }, [fetchActivityStream]); + const pushHistoryState = urlParams => { + let searchParams = parseQueryString(QS_CONFIG, location.search); + searchParams = replaceParams(searchParams, { page: 1 }); + const encodedParams = encodeNonDefaultQueryString(QS_CONFIG, searchParams, { + type: urlParams.get('type'), + }); + history.push( + encodedParams + ? `${location.pathname}?${encodedParams}` + : location.pathname + ); + }; + return ( Date: Tue, 19 Jan 2021 10:07:44 -0500 Subject: [PATCH 10/17] fix spacing issues with activity stream description builder --- .../src/screens/ActivityStream/buildActivityDescription.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx b/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx index 645923a9cb..95d613a44a 100644 --- a/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx +++ b/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx @@ -131,7 +131,7 @@ const buildAnchor = (obj, resource, activity) => { }; const getPastTense = item => { - return /e$/.test(item) ? `${item}d ` : `${item}ed `; + return /e$/.test(item) ? `${item}d` : `${item}ed`; }; const isGroupRelationship = item => { @@ -232,7 +232,7 @@ export default (activity, i18n) => { ); labeledLinks.push( buildLabeledLink( - `${activity.summary_fields.role[0].role_field} to `, + `${activity.summary_fields.role[0].role_field} to`, buildAnchor( activity.summary_fields.group[0], object1, @@ -253,7 +253,7 @@ export default (activity, i18n) => { ); labeledLinks.push( buildLabeledLink( - `${activity.summary_fields.role[0].role_field} to `, + `${activity.summary_fields.role[0].role_field} to`, buildAnchor( activity.summary_fields[object1][0], object1, From 8d46e786062355a8f6a134abdb2fc04cfa691b7b Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 19 Jan 2021 11:48:14 -0500 Subject: [PATCH 11/17] fix lint issue with urlParam name --- awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx index bd36e33821..13cd573fe3 100644 --- a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -94,11 +94,11 @@ function ActivityStream({ i18n }) { fetchActivityStream(); }, [fetchActivityStream]); - const pushHistoryState = urlParams => { + const pushHistoryState = urlParamsToAdd => { let searchParams = parseQueryString(QS_CONFIG, location.search); searchParams = replaceParams(searchParams, { page: 1 }); const encodedParams = encodeNonDefaultQueryString(QS_CONFIG, searchParams, { - type: urlParams.get('type'), + type: urlParamsToAdd.get('type'), }); history.push( encodedParams From a0ded889f9c1d219e0863dcba6f4682bdb22f6d8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 19 Jan 2021 11:49:58 -0500 Subject: [PATCH 12/17] add min height to title to keep page redrwaw from flashing --- .../src/components/ScreenHeader/ScreenHeader.jsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx index 791e72b78e..d856a430ee 100644 --- a/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx +++ b/awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx @@ -39,9 +39,15 @@ const ScreenHeader = ({ breadcrumbConfig, i18n, streamType }) => {
)} - - - +
+ + + +
{streamType !== 'none' && (
From 7a3002f2180d8e89e0af82a4b57981b589ebd2f9 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 19 Jan 2021 11:51:10 -0500 Subject: [PATCH 13/17] fix routes with breadcrumb issues: team roles title crumb missing various inventory crums missing make it so inventories and templates don't get rid of data needed to generate the crumb config --- .../src/screens/Inventory/Inventories.jsx | 96 ++++++++++++------- awx/ui_next/src/screens/Team/Teams.jsx | 1 + .../src/screens/Template/Templates.jsx | 20 +++- 3 files changed, 79 insertions(+), 38 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 947a9be42a..06e793f962 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Route, Switch } from 'react-router-dom'; @@ -12,14 +12,31 @@ import InventoryAdd from './InventoryAdd'; import SmartInventoryAdd from './SmartInventoryAdd'; function Inventories({ i18n }) { - const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + const initScreenHeader = useRef({ '/inventories': i18n._(t`Inventories`), '/inventories/inventory/add': i18n._(t`Create new inventory`), '/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`), }); - const buildBreadcrumbConfig = useCallback( - (inventory, nested, schedule) => { + const [breadcrumbConfig, setScreenHeader] = useState( + initScreenHeader.current + ); + + const [inventory, setInventory] = useState(); + const [nestedObject, setNestedGroup] = useState(); + const [schedule, setSchedule] = useState(); + + const setBreadcrumbConfig = useCallback( + (passedInventory, passedNestedObject, passedSchedule) => { + if (passedInventory && passedInventory.id !== inventory?.id) { + setInventory(passedInventory); + } + if (passedNestedObject && passedNestedObject.id !== nestedObject?.id) { + setNestedGroup(passedNestedObject); + } + if (passedSchedule && passedSchedule.id !== schedule?.id) { + setSchedule(passedSchedule); + } if (!inventory) { return; } @@ -32,13 +49,8 @@ function Inventories({ i18n }) { const inventoryGroupsPath = `${inventoryPath}/groups`; const inventorySourcesPath = `${inventoryPath}/sources`; - setBreadcrumbConfig({ - '/inventories': i18n._(t`Inventories`), - '/inventories/inventory/add': i18n._(t`Create new inventory`), - '/inventories/smart_inventory/add': i18n._( - t`Create new smart inventory` - ), - + setScreenHeader({ + ...initScreenHeader.current, [inventoryPath]: `${inventory.name}`, [`${inventoryPath}/access`]: i18n._(t`Access`), [`${inventoryPath}/completed_jobs`]: i18n._(t`Completed jobs`), @@ -47,50 +59,66 @@ function Inventories({ i18n }) { [inventoryHostsPath]: i18n._(t`Hosts`), [`${inventoryHostsPath}/add`]: i18n._(t`Create new host`), - [`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`, - [`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`), - [`${inventoryHostsPath}/${nested?.id}/details`]: i18n._( + [`${inventoryHostsPath}/${nestedObject?.id}`]: `${nestedObject?.name}`, + [`${inventoryHostsPath}/${nestedObject?.id}/edit`]: i18n._( + t`Edit details` + ), + [`${inventoryHostsPath}/${nestedObject?.id}/details`]: i18n._( t`Host details` ), - [`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._( + [`${inventoryHostsPath}/${nestedObject?.id}/completed_jobs`]: i18n._( t`Completed jobs` ), - [`${inventoryHostsPath}/${nested?.id}/facts`]: i18n._(t`Facts`), - [`${inventoryHostsPath}/${nested?.id}/groups`]: i18n._(t`Groups`), + [`${inventoryHostsPath}/${nestedObject?.id}/facts`]: i18n._(t`Facts`), + [`${inventoryHostsPath}/${nestedObject?.id}/groups`]: i18n._(t`Groups`), [inventoryGroupsPath]: i18n._(t`Groups`), [`${inventoryGroupsPath}/add`]: i18n._(t`Create new group`), - [`${inventoryGroupsPath}/${nested?.id}`]: `${nested?.name}`, - [`${inventoryGroupsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`), - [`${inventoryGroupsPath}/${nested?.id}/details`]: i18n._( + [`${inventoryGroupsPath}/${nestedObject?.id}`]: `${nestedObject?.name}`, + [`${inventoryGroupsPath}/${nestedObject?.id}/edit`]: i18n._( + t`Edit details` + ), + [`${inventoryGroupsPath}/${nestedObject?.id}/details`]: i18n._( t`Group details` ), - [`${inventoryGroupsPath}/${nested?.id}/nested_hosts`]: i18n._(t`Hosts`), - [`${inventoryGroupsPath}/${nested?.id}/nested_hosts/add`]: i18n._( + [`${inventoryGroupsPath}/${nestedObject?.id}/nested_hosts`]: i18n._( + t`Hosts` + ), + [`${inventoryGroupsPath}/${nestedObject?.id}/nested_hosts/add`]: i18n._( t`Create new host` ), - [`${inventoryGroupsPath}/${nested?.id}/nested_groups`]: i18n._( - t`Groups` + [`${inventoryGroupsPath}/${nestedObject?.id}/nested_groups`]: i18n._( + t`Related Groups` ), - [`${inventoryGroupsPath}/${nested?.id}/nested_groups/add`]: i18n._( + [`${inventoryGroupsPath}/${nestedObject?.id}/nested_groups/add`]: i18n._( t`Create new group` ), [`${inventorySourcesPath}`]: i18n._(t`Sources`), [`${inventorySourcesPath}/add`]: i18n._(t`Create new source`), - [`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`, - [`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`), - [`${inventorySourcesPath}/${nested?.id}/edit`]: i18n._(t`Edit details`), - [`${inventorySourcesPath}/${nested?.id}/schedules`]: i18n._( + [`${inventorySourcesPath}/${nestedObject?.id}`]: `${nestedObject?.name}`, + [`${inventorySourcesPath}/${nestedObject?.id}/details`]: i18n._( + t`Details` + ), + [`${inventorySourcesPath}/${nestedObject?.id}/edit`]: i18n._( + t`Edit details` + ), + [`${inventorySourcesPath}/${nestedObject?.id}/schedules`]: i18n._( t`Schedules` ), - [`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`, - [`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}/details`]: i18n._( + [`${inventorySourcesPath}/${nestedObject?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`, + [`${inventorySourcesPath}/${nestedObject?.id}/schedules/add`]: i18n._( + t`Create New Schedule` + ), + [`${inventorySourcesPath}/${nestedObject?.id}/schedules/${schedule?.id}/details`]: i18n._( t`Schedule details` ), + [`${inventorySourcesPath}/${nestedObject?.id}/notifications`]: i18n._( + t`Notifcations` + ), }); }, - [i18n] + [i18n, inventory, nestedObject, schedule] ); return ( @@ -109,12 +137,12 @@ function Inventories({ i18n }) { {({ me }) => ( - + )} - + diff --git a/awx/ui_next/src/screens/Team/Teams.jsx b/awx/ui_next/src/screens/Team/Teams.jsx index f71098752a..0797a6685e 100644 --- a/awx/ui_next/src/screens/Team/Teams.jsx +++ b/awx/ui_next/src/screens/Team/Teams.jsx @@ -29,6 +29,7 @@ function Teams({ i18n }) { [`/teams/${team.id}/details`]: i18n._(t`Details`), [`/teams/${team.id}/users`]: i18n._(t`Users`), [`/teams/${team.id}/access`]: i18n._(t`Access`), + [`/teams/${team.id}/roles`]: i18n._(t`Roles`), }); }, [i18n] diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx index a3e819c151..ae38afd414 100644 --- a/awx/ui_next/src/screens/Template/Templates.jsx +++ b/awx/ui_next/src/screens/Template/Templates.jsx @@ -22,8 +22,18 @@ function Templates({ i18n }) { const [breadcrumbConfig, setScreenHeader] = useState( initScreenHeader.current ); + + const [schedule, setSchedule] = useState(); + const [template, setTemplate] = useState(); + const setBreadcrumbConfig = useCallback( - (template, schedule) => { + (passedTemplate, passedSchedule) => { + if (passedTemplate && passedTemplate.id !== template?.id) { + setTemplate(passedTemplate); + } + if (passedSchedule && passedSchedule.id !== schedule?.id) { + setSchedule(passedSchedule); + } if (!template) return; const templatePath = `/templates/${template.type}/${template.id}`; const schedulesPath = `${templatePath}/schedules`; @@ -42,11 +52,13 @@ function Templates({ i18n }) { [schedulesPath]: i18n._(t`Schedules`), [`${schedulesPath}/add`]: i18n._(t`Create New Schedule`), [`${schedulesPath}/${schedule?.id}`]: `${schedule?.name}`, - [`${schedulesPath}/details`]: i18n._(t`Schedule Details`), - [`${schedulesPath}/edit`]: i18n._(t`Edit Details`), + [`${schedulesPath}/${schedule?.id}/details`]: i18n._( + t`Schedule Details` + ), + [`${schedulesPath}/${schedule?.id}/edit`]: i18n._(t`Edit Schedule`), }); }, - [i18n] + [i18n, template, schedule] ); return ( From f07818f04a7c70923dd7817e2b891ca7e7064291 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 20 Jan 2021 09:14:42 -0500 Subject: [PATCH 14/17] check if breadcrumbs should update by name (which changes), not ID --- awx/ui_next/src/screens/Inventory/Inventories.jsx | 6 +++--- awx/ui_next/src/screens/Template/Templates.jsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 06e793f962..e137032ea4 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -28,13 +28,13 @@ function Inventories({ i18n }) { const setBreadcrumbConfig = useCallback( (passedInventory, passedNestedObject, passedSchedule) => { - if (passedInventory && passedInventory.id !== inventory?.id) { + if (passedInventory && passedInventory.name !== inventory?.name) { setInventory(passedInventory); } - if (passedNestedObject && passedNestedObject.id !== nestedObject?.id) { + if (passedNestedObject && passedNestedObject.name !== nestedObject?.name) { setNestedGroup(passedNestedObject); } - if (passedSchedule && passedSchedule.id !== schedule?.id) { + if (passedSchedule && passedSchedule.name !== schedule?.name) { setSchedule(passedSchedule); } if (!inventory) { diff --git a/awx/ui_next/src/screens/Template/Templates.jsx b/awx/ui_next/src/screens/Template/Templates.jsx index ae38afd414..f3905608cc 100644 --- a/awx/ui_next/src/screens/Template/Templates.jsx +++ b/awx/ui_next/src/screens/Template/Templates.jsx @@ -28,10 +28,10 @@ function Templates({ i18n }) { const setBreadcrumbConfig = useCallback( (passedTemplate, passedSchedule) => { - if (passedTemplate && passedTemplate.id !== template?.id) { + if (passedTemplate && passedTemplate.name !== template?.name) { setTemplate(passedTemplate); } - if (passedSchedule && passedSchedule.id !== schedule?.id) { + if (passedSchedule && passedSchedule.name !== schedule?.name) { setSchedule(passedSchedule); } if (!template) return; From 06ff178f9eff56376e201d97a87f6022ceafb83a Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 20 Jan 2021 10:05:35 -0500 Subject: [PATCH 15/17] update activity stream file structure to be consistent with other routes and aid in testing --- ...ription.jsx => ActivityStreamDescription.jsx} | 7 +++++-- .../ActivityStreamDescription.test.jsx | 12 ++++++++++++ ...Button.jsx => ActivityStreamDetailButton.jsx} | 16 +++++++++------- ...t.jsx => ActivityStreamDetailButton.test.jsx} | 6 +++--- .../ActivityStream/ActivityStreamListItem.jsx | 8 ++++---- .../buildActivityDescription.test.jsx | 10 ---------- .../src/screens/Inventory/Inventories.jsx | 5 ++++- 7 files changed, 37 insertions(+), 27 deletions(-) rename awx/ui_next/src/screens/ActivityStream/{buildActivityDescription.jsx => ActivityStreamDescription.jsx} (99%) create mode 100644 awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.test.jsx rename awx/ui_next/src/screens/ActivityStream/{StreamDetailButton.jsx => ActivityStreamDetailButton.jsx} (82%) rename awx/ui_next/src/screens/ActivityStream/{StreamDetailButton.test.jsx => ActivityStreamDetailButton.test.jsx} (73%) delete mode 100644 awx/ui_next/src/screens/ActivityStream/buildActivityDescription.test.jsx diff --git a/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.jsx similarity index 99% rename from awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx rename to awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.jsx index 95d613a44a..d933e0c259 100644 --- a/awx/ui_next/src/screens/ActivityStream/buildActivityDescription.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; const buildAnchor = (obj, resource, activity) => { let url; @@ -150,7 +151,7 @@ const buildLabeledLink = (label, link) => { ); }; -export default (activity, i18n) => { +function ActivityStreamDescription({ i18n, activity }) { const labeledLinks = []; // Activity stream objects will outlive the resources they reference // in that case, summary_fields will not be available - show generic error text instead @@ -578,4 +579,6 @@ export default (activity, i18n) => { )} ); -}; +} + +export default withI18n()(ActivityStreamDescription); diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.test.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.test.jsx new file mode 100644 index 0000000000..9f3a2982d0 --- /dev/null +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDescription.test.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import ActivityStreamDescription from './ActivityStreamDescription'; + +describe('ActivityStreamDescription', () => { + test('initially renders succesfully', () => { + const description = mountWithContexts( + + ); + expect(description.find('span').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.jsx similarity index 82% rename from awx/ui_next/src/screens/ActivityStream/StreamDetailButton.jsx rename to awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.jsx index b2e8dc368b..22559831b2 100644 --- a/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.jsx @@ -9,7 +9,7 @@ import { formatDateString } from '../../util/dates'; import { DetailList, Detail } from '../../components/DetailList'; import { VariablesDetail } from '../../components/CodeMirrorInput'; -function StreamDetailButton({ i18n, streamItem, user, description }) { +function ActivityStreamDetailButton({ i18n, streamItem, user, description }) { const [isOpen, setIsOpen] = useState(false); const setting = streamItem?.summary_fields?.setting; @@ -50,15 +50,17 @@ function StreamDetailButton({ i18n, streamItem, user, description }) { value={setting && setting[0]?.name} /> - + {streamItem?.changes && ( + + )} ); } -export default withI18n()(StreamDetailButton); +export default withI18n()(ActivityStreamDetailButton); diff --git a/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.test.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.test.jsx similarity index 73% rename from awx/ui_next/src/screens/ActivityStream/StreamDetailButton.test.jsx rename to awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.test.jsx index 04c112c9e0..40dc104117 100644 --- a/awx/ui_next/src/screens/ActivityStream/StreamDetailButton.test.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStreamDetailButton.test.jsx @@ -2,14 +2,14 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; -import StreamDetailButton from './StreamDetailButton'; +import ActivityStreamDetailButton from './ActivityStreamDetailButton'; jest.mock('../../api/models/ActivityStream'); -describe('', () => { +describe('', () => { test('initially renders succesfully', () => { mountWithContexts( - ; return ( @@ -48,7 +48,7 @@ function ActivityStreamListItem({ streamItem, i18n }) { - { - test('initially renders succesfully', () => { - const description = mount(buildDescription({}, {})); - expect(description.find('span').length).toBe(1); - }); -}); diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index e137032ea4..17ee02b3be 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -31,7 +31,10 @@ function Inventories({ i18n }) { if (passedInventory && passedInventory.name !== inventory?.name) { setInventory(passedInventory); } - if (passedNestedObject && passedNestedObject.name !== nestedObject?.name) { + if ( + passedNestedObject && + passedNestedObject.name !== nestedObject?.name + ) { setNestedGroup(passedNestedObject); } if (passedSchedule && passedSchedule.name !== schedule?.name) { From 00e837c17ce2474b08be8ed4821b8ef2dbb3e1f5 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 21 Jan 2021 10:01:39 -0500 Subject: [PATCH 16/17] update grouping of activity stream type select and remove inv scripts --- .../screens/ActivityStream/ActivityStream.jsx | 136 ++++++++++-------- 1 file changed, 73 insertions(+), 63 deletions(-) diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx index 13cd573fe3..c4199d6304 100644 --- a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -6,6 +6,7 @@ import { Card, PageSection, PageSectionVariants, + SelectGroup, Select, SelectVariant, SelectOption, @@ -119,6 +120,7 @@ function ActivityStream({ i18n }) { From 7d495713eedd556b111b62eebfa69e0cc581dd64 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 21 Jan 2021 15:31:08 -0500 Subject: [PATCH 17/17] updated aria label for activity stream type select --- awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx index c4199d6304..84dd9a6066 100644 --- a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -37,7 +37,7 @@ function ActivityStream({ i18n }) { const history = useHistory(); const urlParams = new URLSearchParams(location.search); - const activityStreamType = urlParams.get('type'); + const activityStreamType = urlParams.get('type') || 'all'; let typeParams = {}; @@ -118,11 +118,14 @@ function ActivityStream({ i18n }) { {i18n._(t`Activity Stream`)} +