mirror of
https://github.com/ansible/awx.git
synced 2026-05-15 13:27:40 -02:30
Merge pull request #9083 from jlmitch5/actStream
Update Breadcrumbs/Add Activity Stream UI Reviewed-by: John Hill <johill@redhat.com> https://github.com/unlikelyzero
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import ActivityStream from './models/ActivityStream';
|
||||||
import AdHocCommands from './models/AdHocCommands';
|
import AdHocCommands from './models/AdHocCommands';
|
||||||
import Applications from './models/Applications';
|
import Applications from './models/Applications';
|
||||||
import Auth from './models/Auth';
|
import Auth from './models/Auth';
|
||||||
@@ -39,6 +40,7 @@ import WorkflowJobTemplateNodes from './models/WorkflowJobTemplateNodes';
|
|||||||
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
|
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
|
||||||
import WorkflowJobs from './models/WorkflowJobs';
|
import WorkflowJobs from './models/WorkflowJobs';
|
||||||
|
|
||||||
|
const ActivityStreamAPI = new ActivityStream();
|
||||||
const AdHocCommandsAPI = new AdHocCommands();
|
const AdHocCommandsAPI = new AdHocCommands();
|
||||||
const ApplicationsAPI = new Applications();
|
const ApplicationsAPI = new Applications();
|
||||||
const AuthAPI = new Auth();
|
const AuthAPI = new Auth();
|
||||||
@@ -81,6 +83,7 @@ const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
|
|||||||
const WorkflowJobsAPI = new WorkflowJobs();
|
const WorkflowJobsAPI = new WorkflowJobs();
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
ActivityStreamAPI,
|
||||||
AdHocCommandsAPI,
|
AdHocCommandsAPI,
|
||||||
ApplicationsAPI,
|
ApplicationsAPI,
|
||||||
AuthAPI,
|
AuthAPI,
|
||||||
|
|||||||
10
awx/ui_next/src/api/models/ActivityStream.js
Normal file
10
awx/ui_next/src/api/models/ActivityStream.js
Normal file
@@ -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;
|
||||||
@@ -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 (
|
|
||||||
<PageSection variant={light}>
|
|
||||||
<Breadcrumb>
|
|
||||||
<Route path="/:path">
|
|
||||||
<Crumb breadcrumbConfig={breadcrumbConfig} />
|
|
||||||
</Route>
|
|
||||||
</Breadcrumb>
|
|
||||||
</PageSection>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Crumb = ({ breadcrumbConfig, showDivider }) => {
|
|
||||||
const match = useRouteMatch();
|
|
||||||
const crumb = breadcrumbConfig[match.url];
|
|
||||||
|
|
||||||
let crumbElement = (
|
|
||||||
<BreadcrumbItem key={match.url} showDivider={showDivider}>
|
|
||||||
<Link to={match.url}>{crumb}</Link>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (match.isExact) {
|
|
||||||
crumbElement = (
|
|
||||||
<BreadcrumbHeading key="breadcrumb-heading" showDivider={showDivider}>
|
|
||||||
{crumb}
|
|
||||||
</BreadcrumbHeading>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!crumb) {
|
|
||||||
crumbElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{crumbElement}
|
|
||||||
<Route path={`${match.url}/:path`}>
|
|
||||||
<Crumb breadcrumbConfig={breadcrumbConfig} showDivider />
|
|
||||||
</Route>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Breadcrumbs.propTypes = {
|
|
||||||
breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
Crumb.propTypes = {
|
|
||||||
breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Breadcrumbs;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './Breadcrumbs';
|
|
||||||
@@ -85,7 +85,12 @@ class ListHeader extends React.Component {
|
|||||||
pushHistoryState(params) {
|
pushHistoryState(params) {
|
||||||
const { history, qsConfig } = this.props;
|
const { history, qsConfig } = this.props;
|
||||||
const { pathname } = history.location;
|
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);
|
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,12 @@ function PaginatedDataList({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pushHistoryState = params => {
|
const pushHistoryState = params => {
|
||||||
const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
|
const nonNamespacedParams = parseQueryString({}, history.location.search);
|
||||||
|
const encodedParams = encodeNonDefaultQueryString(
|
||||||
|
qsConfig,
|
||||||
|
params,
|
||||||
|
nonNamespacedParams
|
||||||
|
);
|
||||||
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
|
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ export default function HeaderRow({ qsConfig, children }) {
|
|||||||
order_by: order === 'asc' ? key : `-${key}`,
|
order_by: order === 'asc' ? key : `-${key}`,
|
||||||
page: null,
|
page: null,
|
||||||
});
|
});
|
||||||
const encodedParams = encodeNonDefaultQueryString(qsConfig, newParams);
|
const nonNamespacedParams = parseQueryString({}, history.location.search);
|
||||||
|
const encodedParams = encodeNonDefaultQueryString(
|
||||||
|
qsConfig,
|
||||||
|
newParams,
|
||||||
|
nonNamespacedParams
|
||||||
|
);
|
||||||
history.push(
|
history.push(
|
||||||
encodedParams
|
encodedParams
|
||||||
? `${location.pathname}?${encodedParams}`
|
? `${location.pathname}?${encodedParams}`
|
||||||
|
|||||||
@@ -40,8 +40,13 @@ function PaginatedTable({
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const pushHistoryState = params => {
|
const pushHistoryState = params => {
|
||||||
const { pathname } = history.location;
|
const { pathname, search } = history.location;
|
||||||
const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
|
const nonNamespacedParams = parseQueryString({}, search);
|
||||||
|
const encodedParams = encodeNonDefaultQueryString(
|
||||||
|
qsConfig,
|
||||||
|
params,
|
||||||
|
nonNamespacedParams
|
||||||
|
);
|
||||||
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
|
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
135
awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx
Normal file
135
awx/ui_next/src/components/ScreenHeader/ScreenHeader.jsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
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 (
|
||||||
|
<PageSection variant={light}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{!isOnlyOneCrumb && (
|
||||||
|
<Breadcrumb>
|
||||||
|
<Route path="/:path">
|
||||||
|
<Crumb breadcrumbConfig={breadcrumbConfig} />
|
||||||
|
</Route>
|
||||||
|
</Breadcrumb>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: '31px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Route path="/:path">
|
||||||
|
<ActualTitle breadcrumbConfig={breadcrumbConfig} />
|
||||||
|
</Route>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{streamType !== 'none' && (
|
||||||
|
<div>
|
||||||
|
<Tooltip content={i18n._(t`View activity stream`)} position="top">
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`View activity stream`)}
|
||||||
|
variant="plain"
|
||||||
|
component={Link}
|
||||||
|
to={`/activity_stream${
|
||||||
|
streamType ? `?type=${streamType}` : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<HistoryIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ActualTitle = ({ breadcrumbConfig }) => {
|
||||||
|
const match = useRouteMatch();
|
||||||
|
const title = breadcrumbConfig[match.url];
|
||||||
|
let titleElement;
|
||||||
|
|
||||||
|
if (match.isExact) {
|
||||||
|
titleElement = (
|
||||||
|
<Title size="2xl" headingLevel="h2">
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
titleElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{titleElement}
|
||||||
|
<Route path={`${match.url}/:path`}>
|
||||||
|
<ActualTitle breadcrumbConfig={breadcrumbConfig} />
|
||||||
|
</Route>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Crumb = ({ breadcrumbConfig, showDivider }) => {
|
||||||
|
const match = useRouteMatch();
|
||||||
|
const crumb = breadcrumbConfig[match.url];
|
||||||
|
|
||||||
|
let crumbElement = (
|
||||||
|
<BreadcrumbItem key={match.url} showDivider={showDivider}>
|
||||||
|
<Link to={match.url}>{crumb}</Link>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (match.isExact) {
|
||||||
|
crumbElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!crumb) {
|
||||||
|
crumbElement = null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{crumbElement}
|
||||||
|
<Route path={`${match.url}/:path`}>
|
||||||
|
<Crumb breadcrumbConfig={breadcrumbConfig} showDivider />
|
||||||
|
</Route>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ScreenHeader.propTypes = {
|
||||||
|
breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
Crumb.propTypes = {
|
||||||
|
breadcrumbConfig: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(ScreenHeader);
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import Breadcrumbs from './Breadcrumbs';
|
|
||||||
|
|
||||||
describe('<Breadcrumb />', () => {
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
|
import ScreenHeader from './ScreenHeader';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('<ScreenHeader />', () => {
|
||||||
let breadcrumbWrapper;
|
let breadcrumbWrapper;
|
||||||
let breadcrumb;
|
let breadcrumb;
|
||||||
let breadcrumbItem;
|
let breadcrumbItem;
|
||||||
@@ -17,15 +23,15 @@ describe('<Breadcrumb />', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findChildren = () => {
|
const findChildren = () => {
|
||||||
breadcrumb = breadcrumbWrapper.find('Breadcrumb');
|
breadcrumb = breadcrumbWrapper.find('ScreenHeader');
|
||||||
breadcrumbItem = breadcrumbWrapper.find('BreadcrumbItem');
|
breadcrumbItem = breadcrumbWrapper.find('BreadcrumbItem');
|
||||||
breadcrumbHeading = breadcrumbWrapper.find('BreadcrumbHeading');
|
breadcrumbHeading = breadcrumbWrapper.find('Title');
|
||||||
};
|
};
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
breadcrumbWrapper = mount(
|
breadcrumbWrapper = mountWithContexts(
|
||||||
<MemoryRouter initialEntries={['/foo/1/bar']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/foo/1/bar']} initialIndex={0}>
|
||||||
<Breadcrumbs breadcrumbConfig={config} />
|
<ScreenHeader streamType="all_activity" breadcrumbConfig={config} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -51,9 +57,9 @@ describe('<Breadcrumb />', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
routes.forEach(([location, crumbLength]) => {
|
routes.forEach(([location, crumbLength]) => {
|
||||||
breadcrumbWrapper = mount(
|
breadcrumbWrapper = mountWithContexts(
|
||||||
<MemoryRouter initialEntries={[location]}>
|
<MemoryRouter initialEntries={[location]}>
|
||||||
<Breadcrumbs breadcrumbConfig={config} />
|
<ScreenHeader streamType="all_activity" breadcrumbConfig={config} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
1
awx/ui_next/src/components/ScreenHeader/index.js
Normal file
1
awx/ui_next/src/components/ScreenHeader/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ScreenHeader';
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
|
import ActivityStream from './screens/ActivityStream';
|
||||||
import Applications from './screens/Application';
|
import Applications from './screens/Application';
|
||||||
import Credentials from './screens/Credential';
|
import Credentials from './screens/Credential';
|
||||||
import CredentialTypes from './screens/CredentialType';
|
import CredentialTypes from './screens/CredentialType';
|
||||||
@@ -44,6 +45,11 @@ function getRouteConfig(i18n) {
|
|||||||
path: '/schedules',
|
path: '/schedules',
|
||||||
screen: Schedules,
|
screen: Schedules,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18n._(t`Activity Stream`),
|
||||||
|
path: '/activity_stream',
|
||||||
|
screen: ActivityStream,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Workflow Approvals`),
|
title: i18n._(t`Workflow Approvals`),
|
||||||
path: '/workflow_approvals',
|
path: '/workflow_approvals',
|
||||||
|
|||||||
269
awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx
Normal file
269
awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
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,
|
||||||
|
SelectGroup,
|
||||||
|
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,
|
||||||
|
replaceParams,
|
||||||
|
encodeNonDefaultQueryString,
|
||||||
|
} 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') || 'all';
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const pushHistoryState = urlParamsToAdd => {
|
||||||
|
let searchParams = parseQueryString(QS_CONFIG, location.search);
|
||||||
|
searchParams = replaceParams(searchParams, { page: 1 });
|
||||||
|
const encodedParams = encodeNonDefaultQueryString(QS_CONFIG, searchParams, {
|
||||||
|
type: urlParamsToAdd.get('type'),
|
||||||
|
});
|
||||||
|
history.push(
|
||||||
|
encodedParams
|
||||||
|
? `${location.pathname}?${encodedParams}`
|
||||||
|
: location.pathname
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<PageSection
|
||||||
|
variant={light}
|
||||||
|
className="pf-m-condensed"
|
||||||
|
style={{ display: 'flex', justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<Title size="2xl" headingLevel="h2">
|
||||||
|
{i18n._(t`Activity Stream`)}
|
||||||
|
</Title>
|
||||||
|
<span id="grouped-type-select-id" hidden>
|
||||||
|
{i18n._(t`Activity Stream type selector`)}
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
width="250px"
|
||||||
|
maxHeight="480px"
|
||||||
|
variant={SelectVariant.single}
|
||||||
|
aria-labelledby="grouped-type-select-id"
|
||||||
|
className="activityTypeSelect"
|
||||||
|
onToggle={setIsTypeDropdownOpen}
|
||||||
|
onSelect={(event, selection) => {
|
||||||
|
if (selection) {
|
||||||
|
urlParams.set('type', selection);
|
||||||
|
}
|
||||||
|
setIsTypeDropdownOpen(false);
|
||||||
|
pushHistoryState(urlParams);
|
||||||
|
}}
|
||||||
|
selections={activityStreamType}
|
||||||
|
isOpen={isTypeDropdownOpen}
|
||||||
|
isGrouped
|
||||||
|
>
|
||||||
|
<SelectGroup label={i18n._(t`Views`)} key="views">
|
||||||
|
<SelectOption key="all_activity" value="all">
|
||||||
|
{i18n._(t`Dashboard (all activity)`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="jobs" value="job">
|
||||||
|
{i18n._(t`Jobs`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="schedules" value="schedule">
|
||||||
|
{i18n._(t`Schedules`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="workflow_approvals" value="workflow_approval">
|
||||||
|
{i18n._(t`Workflow Approvals`)}
|
||||||
|
</SelectOption>
|
||||||
|
</SelectGroup>
|
||||||
|
<SelectGroup label={i18n._(t`Resources`)} key="resources">
|
||||||
|
<SelectOption
|
||||||
|
key="templates"
|
||||||
|
value="job_template,workflow_job_template,workflow_job_template_node"
|
||||||
|
>
|
||||||
|
{i18n._(t`Templates`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="credentials" value="credential">
|
||||||
|
{i18n._(t`Credentials`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="projects" value="project">
|
||||||
|
{i18n._(t`Projects`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="inventories" value="inventory">
|
||||||
|
{i18n._(t`Inventories`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="hosts" value="host">
|
||||||
|
{i18n._(t`Hosts`)}
|
||||||
|
</SelectOption>
|
||||||
|
</SelectGroup>
|
||||||
|
<SelectGroup label={i18n._(t`Access`)} key="access">
|
||||||
|
<SelectOption key="organizations" value="organization">
|
||||||
|
{i18n._(t`Organizations`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="users" value="user">
|
||||||
|
{i18n._(t`Users`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="teams" value="team">
|
||||||
|
{i18n._(t`Teams`)}
|
||||||
|
</SelectOption>
|
||||||
|
</SelectGroup>
|
||||||
|
<SelectGroup label={i18n._(t`Adminisration`)} key="administration">
|
||||||
|
<SelectOption key="credential_types" value="credential_type">
|
||||||
|
{i18n._(t`Credential Types`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption
|
||||||
|
key="notification_templates"
|
||||||
|
value="notification_template"
|
||||||
|
>
|
||||||
|
{i18n._(t`Notification Templates`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption key="instance_groups" value="instance_group">
|
||||||
|
{i18n._(t`Instance Groups`)}
|
||||||
|
</SelectOption>
|
||||||
|
<SelectOption
|
||||||
|
key="applications"
|
||||||
|
value="o_auth2_application,o_auth2_access_token"
|
||||||
|
>
|
||||||
|
{i18n._(t`Applications & Tokens`)}
|
||||||
|
</SelectOption>
|
||||||
|
</SelectGroup>
|
||||||
|
<SelectGroup label={i18n._(t`Settings`)} key="settings">
|
||||||
|
<SelectOption key="settings" value="setting">
|
||||||
|
{i18n._(t`Settings`)}
|
||||||
|
</SelectOption>
|
||||||
|
</SelectGroup>
|
||||||
|
</Select>
|
||||||
|
</PageSection>
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<PaginatedTable
|
||||||
|
contentError={contentError}
|
||||||
|
hasContentLoading={isLoading}
|
||||||
|
items={results}
|
||||||
|
itemCount={count}
|
||||||
|
pluralizedItemName={i18n._(t`Events`)}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
toolbarSearchColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Keyword`),
|
||||||
|
key: 'search',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Initiated by (username)`),
|
||||||
|
key: 'actor__username__icontains',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
toolbarSortColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Time`),
|
||||||
|
key: 'timestamp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Initiated by`),
|
||||||
|
key: 'actor__username',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
toolbarSearchableKeys={searchableKeys}
|
||||||
|
toolbarRelatedSearchableKeys={relatedSearchableKeys}
|
||||||
|
headerRow={
|
||||||
|
<HeaderRow qsConfig={QS_CONFIG}>
|
||||||
|
<HeaderCell sortKey="timestamp">{i18n._(t`Time`)}</HeaderCell>
|
||||||
|
<HeaderCell sortKey="actor__username">
|
||||||
|
{i18n._(t`Initiated by`)}
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell>{i18n._(t`Event`)}</HeaderCell>
|
||||||
|
<HeaderCell>{i18n._(t`Actions`)}</HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
}
|
||||||
|
renderToolbar={props => (
|
||||||
|
<DatalistToolbar {...props} qsConfig={QS_CONFIG} />
|
||||||
|
)}
|
||||||
|
renderRow={streamItem => (
|
||||||
|
<ActivityStreamListItem streamItem={streamItem} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(ActivityStream);
|
||||||
@@ -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('<ActivityStream />', () => {
|
||||||
|
let pageWrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pageWrapper = mountWithContexts(<ActivityStream />);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
pageWrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initially renders without crashing', () => {
|
||||||
|
expect(pageWrapper.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
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;
|
||||||
|
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.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) {
|
||||||
|
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 {
|
||||||
|
id: wfjt_id,
|
||||||
|
name: 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 <Link to={url}>{name}</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{name}</span>;
|
||||||
|
} catch (err) {
|
||||||
|
return <span>{obj.name || obj.username || ''}</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<span>
|
||||||
|
{label} {link}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
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 <object2> role_name from <object1>"
|
||||||
|
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 <object2> role_name to <object1>"
|
||||||
|
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 <object2> role_name from <object1>"
|
||||||
|
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 <object2> role_name to <object1>"
|
||||||
|
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 <object2> from <object1>"
|
||||||
|
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 <object2> to <object1>"
|
||||||
|
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 <object1>"
|
||||||
|
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 <span>{i18n._(t`Event summary not available`)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{labeledLinks.reduce(
|
||||||
|
(acc, x) =>
|
||||||
|
acc === null ? (
|
||||||
|
x
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{acc} {x}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
null
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(ActivityStreamDescription);
|
||||||
@@ -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(
|
||||||
|
<ActivityStreamDescription activity={{}} />
|
||||||
|
);
|
||||||
|
expect(description.find('span').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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 ActivityStreamDetailButton({ 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 (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`View event details`)}
|
||||||
|
variant="plain"
|
||||||
|
component="button"
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
>
|
||||||
|
<SearchPlusIcon />
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
variant="large"
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={i18n._(t`Event detail`)}
|
||||||
|
aria-label={i18n._(t`Event detail modal`)}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
<DetailList gutter="sm">
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Time`)}
|
||||||
|
value={formatDateString(streamItem.timestamp)}
|
||||||
|
/>
|
||||||
|
<Detail label={i18n._(t`Initiated by`)} value={user} />
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Setting category`)}
|
||||||
|
value={setting && setting[0]?.category}
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Setting name`)}
|
||||||
|
value={setting && setting[0]?.name}
|
||||||
|
/>
|
||||||
|
<Detail fullWidth label={i18n._(t`Action`)} value={description} />
|
||||||
|
{streamItem?.changes && (
|
||||||
|
<VariablesDetail
|
||||||
|
label={i18n._(t`Changes`)}
|
||||||
|
rows={changeRows}
|
||||||
|
value={streamItem?.changes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DetailList>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(ActivityStreamDetailButton);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
import ActivityStreamDetailButton from './ActivityStreamDetailButton';
|
||||||
|
|
||||||
|
jest.mock('../../api/models/ActivityStream');
|
||||||
|
|
||||||
|
describe('<ActivityStreamDetailButton />', () => {
|
||||||
|
test('initially renders succesfully', () => {
|
||||||
|
mountWithContexts(
|
||||||
|
<ActivityStreamDetailButton
|
||||||
|
streamItem={{
|
||||||
|
timestamp: '12:00:00',
|
||||||
|
}}
|
||||||
|
user={<Link to="/users/1/details">Bob</Link>}
|
||||||
|
description={<span>foo</span>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 ActivityStreamDetailButton from './ActivityStreamDetailButton';
|
||||||
|
import ActivityStreamDescription from './ActivityStreamDescription';
|
||||||
|
|
||||||
|
function ActivityStreamListItem({ streamItem, i18n }) {
|
||||||
|
ActivityStreamListItem.propTypes = {
|
||||||
|
streamItem: shape({}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildUser = item => {
|
||||||
|
let link;
|
||||||
|
if (item?.summary_fields?.actor?.id) {
|
||||||
|
link = (
|
||||||
|
<Link to={`/users/${item.summary_fields.actor.id}/details`}>
|
||||||
|
{item.summary_fields.actor.username}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} 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 = <ActivityStreamDescription activity={streamItem} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr id={streamItem.id} aria-labelledby={labelId}>
|
||||||
|
<Td />
|
||||||
|
<Td dataLabel={i18n._(t`Time`)}>
|
||||||
|
{streamItem.timestamp ? formatDateString(streamItem.timestamp) : ''}
|
||||||
|
</Td>
|
||||||
|
<Td dataLabel={i18n._(t`Initiated By`)}>{user}</Td>
|
||||||
|
<Td id={labelId} dataLabel={i18n._(t`Event`)}>
|
||||||
|
{description}
|
||||||
|
</Td>
|
||||||
|
<ActionsTd dataLabel={i18n._(t`Actions`)}>
|
||||||
|
<ActionItem visible tooltip={i18n._(t`View event details`)}>
|
||||||
|
<ActivityStreamDetailButton
|
||||||
|
streamItem={streamItem}
|
||||||
|
user={user}
|
||||||
|
description={description}
|
||||||
|
/>
|
||||||
|
</ActionItem>
|
||||||
|
</ActionsTd>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default withI18n()(ActivityStreamListItem);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
import ActivityStreamListItem from './ActivityStreamListItem';
|
||||||
|
|
||||||
|
jest.mock('../../api/models/ActivityStream');
|
||||||
|
|
||||||
|
describe('<ActivityStreamListItem />', () => {
|
||||||
|
test('initially renders succesfully', () => {
|
||||||
|
mountWithContexts(
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<ActivityStreamListItem
|
||||||
|
streamItem={{
|
||||||
|
timestamp: '12:00:00',
|
||||||
|
}}
|
||||||
|
onSelect={() => {}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
1
awx/ui_next/src/screens/ActivityStream/index.js
Normal file
1
awx/ui_next/src/screens/ActivityStream/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ActivityStream';
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import ApplicationsList from './ApplicationsList';
|
import ApplicationsList from './ApplicationsList';
|
||||||
import ApplicationAdd from './ApplicationAdd';
|
import ApplicationAdd from './ApplicationAdd';
|
||||||
import Application from './Application';
|
import Application from './Application';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
import { Detail, DetailList } from '../../components/DetailList';
|
import { Detail, DetailList } from '../../components/DetailList';
|
||||||
|
|
||||||
const ApplicationAlert = styled(Alert)`
|
const ApplicationAlert = styled(Alert)`
|
||||||
@@ -45,7 +45,10 @@ function Applications({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="o_auth2_application,o_auth2_access_token"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/applications/add">
|
<Route path="/applications/add">
|
||||||
<ApplicationAdd
|
<ApplicationAdd
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import Applications from './Applications';
|
import Applications from './Applications';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Applications />', () => {
|
describe('<Applications />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Route, Switch } from 'react-router-dom';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
import Credential from './Credential';
|
import Credential from './Credential';
|
||||||
import CredentialAdd from './CredentialAdd';
|
import CredentialAdd from './CredentialAdd';
|
||||||
import { CredentialList } from './CredentialList';
|
import { CredentialList } from './CredentialList';
|
||||||
@@ -34,7 +34,10 @@ function Credentials({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="credential"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/credentials/add">
|
<Route path="/credentials/add">
|
||||||
<Config>{({ me }) => <CredentialAdd me={me || {}} />}</Config>
|
<Config>{({ me }) => <CredentialAdd me={me || {}} />}</Config>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { createMemoryHistory } from 'history';
|
|||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
import Credentials from './Credentials';
|
import Credentials from './Credentials';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Credentials />', () => {
|
describe('<Credentials />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
@@ -30,8 +34,8 @@ describe('<Credentials />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find('Crumb').length).toBe(1);
|
expect(wrapper.find('Crumb').length).toBe(0);
|
||||||
expect(wrapper.find('BreadcrumbHeading').text()).toBe('Credentials');
|
expect(wrapper.find('Title').text()).toBe('Credentials');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should display create new credential breadcrumb heading', () => {
|
test('should display create new credential breadcrumb heading', () => {
|
||||||
@@ -51,8 +55,6 @@ describe('<Credentials />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find('Crumb').length).toBe(2);
|
expect(wrapper.find('Crumb').length).toBe(2);
|
||||||
expect(wrapper.find('BreadcrumbHeading').text()).toBe(
|
expect(wrapper.find('Title').text()).toBe('Create New Credential');
|
||||||
'Create New Credential'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Route, Switch } from 'react-router-dom';
|
|||||||
import CredentialTypeAdd from './CredentialTypeAdd';
|
import CredentialTypeAdd from './CredentialTypeAdd';
|
||||||
import CredentialTypeList from './CredentialTypeList';
|
import CredentialTypeList from './CredentialTypeList';
|
||||||
import CredentialType from './CredentialType';
|
import CredentialType from './CredentialType';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
|
|
||||||
function CredentialTypes({ i18n }) {
|
function CredentialTypes({ i18n }) {
|
||||||
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||||
@@ -33,7 +33,10 @@ function CredentialTypes({ i18n }) {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="credential_type"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/credential_types/add">
|
<Route path="/credential_types/add">
|
||||||
<CredentialTypeAdd />
|
<CredentialTypeAdd />
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import CredentialTypes from './CredentialTypes';
|
import CredentialTypes from './CredentialTypes';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<CredentialTypes/>', () => {
|
describe('<CredentialTypes/>', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
let pageSections;
|
let pageSections;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
|
|
||||||
import useRequest from '../../util/useRequest';
|
import useRequest from '../../util/useRequest';
|
||||||
import { DashboardAPI } from '../../api';
|
import { DashboardAPI } from '../../api';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
import JobList from '../../components/JobList';
|
import JobList from '../../components/JobList';
|
||||||
import ContentLoading from '../../components/ContentLoading';
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
import LineChart from './shared/LineChart';
|
import LineChart from './shared/LineChart';
|
||||||
@@ -117,7 +117,10 @@ function Dashboard({ i18n }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Breadcrumbs breadcrumbConfig={{ '/home': i18n._(t`Dashboard`) }} />
|
<ScreenHeader
|
||||||
|
streamType="all"
|
||||||
|
breadcrumbConfig={{ '/home': i18n._(t`Dashboard`) }}
|
||||||
|
/>
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Counts>
|
<Counts>
|
||||||
<Count
|
<Count
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { DashboardAPI } from '../../api';
|
|||||||
import Dashboard from './Dashboard';
|
import Dashboard from './Dashboard';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Dashboard />', () => {
|
describe('<Dashboard />', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
|
|
||||||
import HostList from './HostList';
|
import HostList from './HostList';
|
||||||
import HostAdd from './HostAdd';
|
import HostAdd from './HostAdd';
|
||||||
@@ -37,7 +37,7 @@ function Hosts({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader streamType="host" breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/hosts/add">
|
<Route path="/hosts/add">
|
||||||
<HostAdd />
|
<HostAdd />
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import Hosts from './Hosts';
|
import Hosts from './Hosts';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Hosts />', () => {
|
describe('<Hosts />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(<Hosts />);
|
mountWithContexts(<Hosts />);
|
||||||
@@ -27,7 +31,7 @@ describe('<Hosts />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find('BreadcrumbHeading').length).toBe(1);
|
expect(wrapper.find('Title').length).toBe(1);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import InstanceGroup from './InstanceGroup';
|
|||||||
|
|
||||||
import ContainerGroupAdd from './ContainerGroupAdd';
|
import ContainerGroupAdd from './ContainerGroupAdd';
|
||||||
import ContainerGroup from './ContainerGroup';
|
import ContainerGroup from './ContainerGroup';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
|
|
||||||
function InstanceGroups({ i18n }) {
|
function InstanceGroups({ i18n }) {
|
||||||
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||||
@@ -54,7 +54,10 @@ function InstanceGroups({ i18n }) {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="instance_group"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/instance_groups/container_group/add">
|
<Route path="/instance_groups/container_group/add">
|
||||||
<ContainerGroupAdd />
|
<ContainerGroupAdd />
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import InstanceGroups from './InstanceGroups';
|
import InstanceGroups from './InstanceGroups';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<InstanceGroups/>', () => {
|
describe('<InstanceGroups/>', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
let pageSections;
|
let pageSections;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useRef } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
import { InventoryList } from './InventoryList';
|
import { InventoryList } from './InventoryList';
|
||||||
import Inventory from './Inventory';
|
import Inventory from './Inventory';
|
||||||
import SmartInventory from './SmartInventory';
|
import SmartInventory from './SmartInventory';
|
||||||
@@ -12,14 +12,34 @@ import InventoryAdd from './InventoryAdd';
|
|||||||
import SmartInventoryAdd from './SmartInventoryAdd';
|
import SmartInventoryAdd from './SmartInventoryAdd';
|
||||||
|
|
||||||
function Inventories({ i18n }) {
|
function Inventories({ i18n }) {
|
||||||
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
const initScreenHeader = useRef({
|
||||||
'/inventories': i18n._(t`Inventories`),
|
'/inventories': i18n._(t`Inventories`),
|
||||||
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
||||||
'/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`),
|
'/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`),
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildBreadcrumbConfig = useCallback(
|
const [breadcrumbConfig, setScreenHeader] = useState(
|
||||||
(inventory, nested, schedule) => {
|
initScreenHeader.current
|
||||||
|
);
|
||||||
|
|
||||||
|
const [inventory, setInventory] = useState();
|
||||||
|
const [nestedObject, setNestedGroup] = useState();
|
||||||
|
const [schedule, setSchedule] = useState();
|
||||||
|
|
||||||
|
const setBreadcrumbConfig = useCallback(
|
||||||
|
(passedInventory, passedNestedObject, passedSchedule) => {
|
||||||
|
if (passedInventory && passedInventory.name !== inventory?.name) {
|
||||||
|
setInventory(passedInventory);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
passedNestedObject &&
|
||||||
|
passedNestedObject.name !== nestedObject?.name
|
||||||
|
) {
|
||||||
|
setNestedGroup(passedNestedObject);
|
||||||
|
}
|
||||||
|
if (passedSchedule && passedSchedule.name !== schedule?.name) {
|
||||||
|
setSchedule(passedSchedule);
|
||||||
|
}
|
||||||
if (!inventory) {
|
if (!inventory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -32,13 +52,8 @@ function Inventories({ i18n }) {
|
|||||||
const inventoryGroupsPath = `${inventoryPath}/groups`;
|
const inventoryGroupsPath = `${inventoryPath}/groups`;
|
||||||
const inventorySourcesPath = `${inventoryPath}/sources`;
|
const inventorySourcesPath = `${inventoryPath}/sources`;
|
||||||
|
|
||||||
setBreadcrumbConfig({
|
setScreenHeader({
|
||||||
'/inventories': i18n._(t`Inventories`),
|
...initScreenHeader.current,
|
||||||
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
|
||||||
'/inventories/smart_inventory/add': i18n._(
|
|
||||||
t`Create new smart inventory`
|
|
||||||
),
|
|
||||||
|
|
||||||
[inventoryPath]: `${inventory.name}`,
|
[inventoryPath]: `${inventory.name}`,
|
||||||
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
||||||
[`${inventoryPath}/completed_jobs`]: i18n._(t`Completed jobs`),
|
[`${inventoryPath}/completed_jobs`]: i18n._(t`Completed jobs`),
|
||||||
@@ -47,55 +62,74 @@ function Inventories({ i18n }) {
|
|||||||
|
|
||||||
[inventoryHostsPath]: i18n._(t`Hosts`),
|
[inventoryHostsPath]: i18n._(t`Hosts`),
|
||||||
[`${inventoryHostsPath}/add`]: i18n._(t`Create new host`),
|
[`${inventoryHostsPath}/add`]: i18n._(t`Create new host`),
|
||||||
[`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`,
|
[`${inventoryHostsPath}/${nestedObject?.id}`]: `${nestedObject?.name}`,
|
||||||
[`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
[`${inventoryHostsPath}/${nestedObject?.id}/edit`]: i18n._(
|
||||||
[`${inventoryHostsPath}/${nested?.id}/details`]: i18n._(
|
t`Edit details`
|
||||||
|
),
|
||||||
|
[`${inventoryHostsPath}/${nestedObject?.id}/details`]: i18n._(
|
||||||
t`Host details`
|
t`Host details`
|
||||||
),
|
),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._(
|
[`${inventoryHostsPath}/${nestedObject?.id}/completed_jobs`]: i18n._(
|
||||||
t`Completed jobs`
|
t`Completed jobs`
|
||||||
),
|
),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/facts`]: i18n._(t`Facts`),
|
[`${inventoryHostsPath}/${nestedObject?.id}/facts`]: i18n._(t`Facts`),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/groups`]: i18n._(t`Groups`),
|
[`${inventoryHostsPath}/${nestedObject?.id}/groups`]: i18n._(t`Groups`),
|
||||||
|
|
||||||
[inventoryGroupsPath]: i18n._(t`Groups`),
|
[inventoryGroupsPath]: i18n._(t`Groups`),
|
||||||
[`${inventoryGroupsPath}/add`]: i18n._(t`Create new group`),
|
[`${inventoryGroupsPath}/add`]: i18n._(t`Create new group`),
|
||||||
[`${inventoryGroupsPath}/${nested?.id}`]: `${nested?.name}`,
|
[`${inventoryGroupsPath}/${nestedObject?.id}`]: `${nestedObject?.name}`,
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
[`${inventoryGroupsPath}/${nestedObject?.id}/edit`]: i18n._(
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/details`]: i18n._(
|
t`Edit details`
|
||||||
|
),
|
||||||
|
[`${inventoryGroupsPath}/${nestedObject?.id}/details`]: i18n._(
|
||||||
t`Group details`
|
t`Group details`
|
||||||
),
|
),
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts`]: i18n._(t`Hosts`),
|
[`${inventoryGroupsPath}/${nestedObject?.id}/nested_hosts`]: i18n._(
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts/add`]: i18n._(
|
t`Hosts`
|
||||||
|
),
|
||||||
|
[`${inventoryGroupsPath}/${nestedObject?.id}/nested_hosts/add`]: i18n._(
|
||||||
t`Create new host`
|
t`Create new host`
|
||||||
),
|
),
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/nested_groups`]: i18n._(
|
[`${inventoryGroupsPath}/${nestedObject?.id}/nested_groups`]: i18n._(
|
||||||
t`Groups`
|
t`Related Groups`
|
||||||
),
|
),
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/nested_groups/add`]: i18n._(
|
[`${inventoryGroupsPath}/${nestedObject?.id}/nested_groups/add`]: i18n._(
|
||||||
t`Create new group`
|
t`Create new group`
|
||||||
),
|
),
|
||||||
|
|
||||||
[`${inventorySourcesPath}`]: i18n._(t`Sources`),
|
[`${inventorySourcesPath}`]: i18n._(t`Sources`),
|
||||||
[`${inventorySourcesPath}/add`]: i18n._(t`Create new source`),
|
[`${inventorySourcesPath}/add`]: i18n._(t`Create new source`),
|
||||||
[`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`,
|
[`${inventorySourcesPath}/${nestedObject?.id}`]: `${nestedObject?.name}`,
|
||||||
[`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`),
|
[`${inventorySourcesPath}/${nestedObject?.id}/details`]: i18n._(
|
||||||
[`${inventorySourcesPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
t`Details`
|
||||||
[`${inventorySourcesPath}/${nested?.id}/schedules`]: i18n._(
|
),
|
||||||
|
[`${inventorySourcesPath}/${nestedObject?.id}/edit`]: i18n._(
|
||||||
|
t`Edit details`
|
||||||
|
),
|
||||||
|
[`${inventorySourcesPath}/${nestedObject?.id}/schedules`]: i18n._(
|
||||||
t`Schedules`
|
t`Schedules`
|
||||||
),
|
),
|
||||||
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`,
|
[`${inventorySourcesPath}/${nestedObject?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`,
|
||||||
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}/details`]: i18n._(
|
[`${inventorySourcesPath}/${nestedObject?.id}/schedules/add`]: i18n._(
|
||||||
|
t`Create New Schedule`
|
||||||
|
),
|
||||||
|
[`${inventorySourcesPath}/${nestedObject?.id}/schedules/${schedule?.id}/details`]: i18n._(
|
||||||
t`Schedule details`
|
t`Schedule details`
|
||||||
),
|
),
|
||||||
|
[`${inventorySourcesPath}/${nestedObject?.id}/notifications`]: i18n._(
|
||||||
|
t`Notifcations`
|
||||||
|
),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[i18n]
|
[i18n, inventory, nestedObject, schedule]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="inventory"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/inventories/inventory/add">
|
<Route path="/inventories/inventory/add">
|
||||||
<InventoryAdd />
|
<InventoryAdd />
|
||||||
@@ -106,12 +140,12 @@ function Inventories({ i18n }) {
|
|||||||
<Route path="/inventories/inventory/:id">
|
<Route path="/inventories/inventory/:id">
|
||||||
<Config>
|
<Config>
|
||||||
{({ me }) => (
|
{({ me }) => (
|
||||||
<Inventory setBreadcrumb={buildBreadcrumbConfig} me={me || {}} />
|
<Inventory setBreadcrumb={setBreadcrumbConfig} me={me || {}} />
|
||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/inventories/smart_inventory/:id">
|
<Route path="/inventories/smart_inventory/:id">
|
||||||
<SmartInventory setBreadcrumb={buildBreadcrumbConfig} />
|
<SmartInventory setBreadcrumb={setBreadcrumbConfig} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/inventories">
|
<Route path="/inventories">
|
||||||
<InventoryList />
|
<InventoryList />
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import Inventories from './Inventories';
|
import Inventories from './Inventories';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Inventories />', () => {
|
describe('<Inventories />', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import Job from './Jobs';
|
import Job from './Jobs';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Job />', () => {
|
describe('<Job />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(<Job />);
|
mountWithContexts(<Job />);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Route, Switch, useParams, useRouteMatch } from 'react-router-dom';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { PageSection } from '@patternfly/react-core';
|
import { PageSection } from '@patternfly/react-core';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
import Job from './Job';
|
import Job from './Job';
|
||||||
import JobTypeRedirect from './JobTypeRedirect';
|
import JobTypeRedirect from './JobTypeRedirect';
|
||||||
import JobList from '../../components/JobList';
|
import JobList from '../../components/JobList';
|
||||||
@@ -40,7 +40,7 @@ function Jobs({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader streamType="job" breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={match.path}>
|
<Route exact path={match.path}>
|
||||||
<PageSection>
|
<PageSection>
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import Jobs from './Jobs';
|
import Jobs from './Jobs';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Jobs />', () => {
|
describe('<Jobs />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(<Jobs />);
|
mountWithContexts(<Jobs />);
|
||||||
@@ -27,7 +31,7 @@ describe('<Jobs />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find('BreadcrumbHeading').length).toBe(1);
|
expect(wrapper.find('Title').length).toBe(1);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import React, { Fragment } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
|
|
||||||
function ManagementJobs({ i18n }) {
|
function ManagementJobs({ i18n }) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Breadcrumbs
|
<ScreenHeader
|
||||||
|
streamType="none"
|
||||||
breadcrumbConfig={{ '/management_jobs': i18n._(t`Management Jobs`) }}
|
breadcrumbConfig={{ '/management_jobs': i18n._(t`Management Jobs`) }}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import ManagementJobs from './ManagementJobs';
|
import ManagementJobs from './ManagementJobs';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<ManagementJobs />', () => {
|
describe('<ManagementJobs />', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
|
|
||||||
@@ -17,6 +21,6 @@ describe('<ManagementJobs />', () => {
|
|||||||
|
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
expect(pageWrapper.length).toBe(1);
|
expect(pageWrapper.length).toBe(1);
|
||||||
expect(pageWrapper.find('Breadcrumbs').length).toBe(1);
|
expect(pageWrapper.find('ScreenHeader').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { t } from '@lingui/macro';
|
|||||||
import NotificationTemplateList from './NotificationTemplateList';
|
import NotificationTemplateList from './NotificationTemplateList';
|
||||||
import NotificationTemplateAdd from './NotificationTemplateAdd';
|
import NotificationTemplateAdd from './NotificationTemplateAdd';
|
||||||
import NotificationTemplate from './NotificationTemplate';
|
import NotificationTemplate from './NotificationTemplate';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
|
|
||||||
function NotificationTemplates({ i18n }) {
|
function NotificationTemplates({ i18n }) {
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
@@ -32,7 +32,10 @@ function NotificationTemplates({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="notification_template"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.url}/add`}>
|
<Route path={`${match.url}/add`}>
|
||||||
<NotificationTemplateAdd />
|
<NotificationTemplateAdd />
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import React from 'react';
|
|||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
import NotificationTemplates from './NotificationTemplates';
|
import NotificationTemplates from './NotificationTemplates';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<NotificationTemplates />', () => {
|
describe('<NotificationTemplates />', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
let pageSections;
|
let pageSections;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
|
|
||||||
import OrganizationsList from './OrganizationList/OrganizationList';
|
import OrganizationsList from './OrganizationList/OrganizationList';
|
||||||
import OrganizationAdd from './OrganizationAdd/OrganizationAdd';
|
import OrganizationAdd from './OrganizationAdd/OrganizationAdd';
|
||||||
@@ -42,7 +42,10 @@ function Organizations({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="organization"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.path}/add`}>
|
<Route path={`${match.path}/add`}>
|
||||||
<OrganizationAdd />
|
<OrganizationAdd />
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
import Organizations from './Organizations';
|
import Organizations from './Organizations';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Organizations />', () => {
|
describe('<Organizations />', () => {
|
||||||
test('initially renders succesfully', async () => {
|
test('initially renders succesfully', async () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Route, withRouter, Switch } from 'react-router-dom';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
|
|
||||||
import ProjectsList from './ProjectList/ProjectList';
|
import ProjectsList from './ProjectList/ProjectList';
|
||||||
import ProjectAdd from './ProjectAdd/ProjectAdd';
|
import ProjectAdd from './ProjectAdd/ProjectAdd';
|
||||||
@@ -45,7 +45,7 @@ function Projects({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader streamType="project" breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/projects/add">
|
<Route path="/projects/add">
|
||||||
<ProjectAdd />
|
<ProjectAdd />
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import Projects from './Projects';
|
import Projects from './Projects';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Projects />', () => {
|
describe('<Projects />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(<Projects />);
|
mountWithContexts(<Projects />);
|
||||||
@@ -27,7 +31,7 @@ describe('<Projects />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find('BreadcrumbHeading').length).toBe(1);
|
expect(wrapper.find('Title').length).toBe(1);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
import { ScheduleList } from '../../components/Schedule';
|
import { ScheduleList } from '../../components/Schedule';
|
||||||
import { SchedulesAPI } from '../../api';
|
import { SchedulesAPI } from '../../api';
|
||||||
|
|
||||||
@@ -19,7 +19,8 @@ function AllSchedules({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs
|
<ScreenHeader
|
||||||
|
streamType="schedule"
|
||||||
breadcrumbConfig={{
|
breadcrumbConfig={{
|
||||||
'/schedules': i18n._(t`Schedules`),
|
'/schedules': i18n._(t`Schedules`),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { createMemoryHistory } from 'history';
|
|||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
import AllSchedules from './AllSchedules';
|
import AllSchedules from './AllSchedules';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<AllSchedules />', () => {
|
describe('<AllSchedules />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
@@ -30,7 +34,6 @@ describe('<AllSchedules />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.find('Crumb').length).toBe(1);
|
expect(wrapper.find('Title').text()).toBe('Schedules');
|
||||||
expect(wrapper.find('BreadcrumbHeading').text()).toBe('Schedules');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { PageSection, Card } from '@patternfly/react-core';
|
import { PageSection, Card } from '@patternfly/react-core';
|
||||||
import ContentError from '../../components/ContentError';
|
import ContentError from '../../components/ContentError';
|
||||||
import ContentLoading from '../../components/ContentLoading';
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
import ActivityStream from './ActivityStream';
|
import ActivityStream from './ActivityStream';
|
||||||
import AzureAD from './AzureAD';
|
import AzureAD from './AzureAD';
|
||||||
import GitHub from './GitHub';
|
import GitHub from './GitHub';
|
||||||
@@ -129,7 +129,7 @@ function Settings({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsProvider value={result}>
|
<SettingsProvider value={result}>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader streamType="setting" breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/settings/activity_stream">
|
<Route path="/settings/activity_stream">
|
||||||
<ActivityStream />
|
<ActivityStream />
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ jest.mock('../../api/models/Settings');
|
|||||||
SettingsAPI.readAllOptions.mockResolvedValue({
|
SettingsAPI.readAllOptions.mockResolvedValue({
|
||||||
data: mockAllOptions,
|
data: mockAllOptions,
|
||||||
});
|
});
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Settings />', () => {
|
describe('<Settings />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader';
|
||||||
import TeamList from './TeamList';
|
import TeamList from './TeamList';
|
||||||
import TeamAdd from './TeamAdd';
|
import TeamAdd from './TeamAdd';
|
||||||
import Team from './Team';
|
import Team from './Team';
|
||||||
@@ -29,6 +29,7 @@ function Teams({ i18n }) {
|
|||||||
[`/teams/${team.id}/details`]: i18n._(t`Details`),
|
[`/teams/${team.id}/details`]: i18n._(t`Details`),
|
||||||
[`/teams/${team.id}/users`]: i18n._(t`Users`),
|
[`/teams/${team.id}/users`]: i18n._(t`Users`),
|
||||||
[`/teams/${team.id}/access`]: i18n._(t`Access`),
|
[`/teams/${team.id}/access`]: i18n._(t`Access`),
|
||||||
|
[`/teams/${team.id}/roles`]: i18n._(t`Roles`),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[i18n]
|
[i18n]
|
||||||
@@ -36,7 +37,7 @@ function Teams({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader streamType="team" breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/teams/add">
|
<Route path="/teams/add">
|
||||||
<TeamAdd />
|
<TeamAdd />
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
import Teams from './Teams';
|
import Teams from './Teams';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Teams />', () => {
|
describe('<Teams />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { Route, withRouter, Switch } from 'react-router-dom';
|
import { Route, withRouter, Switch } from 'react-router-dom';
|
||||||
import { PageSection } from '@patternfly/react-core';
|
import { PageSection } from '@patternfly/react-core';
|
||||||
|
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
import { TemplateList } from './TemplateList';
|
import { TemplateList } from './TemplateList';
|
||||||
import Template from './Template';
|
import Template from './Template';
|
||||||
import WorkflowJobTemplate from './WorkflowJobTemplate';
|
import WorkflowJobTemplate from './WorkflowJobTemplate';
|
||||||
@@ -12,22 +12,34 @@ import JobTemplateAdd from './JobTemplateAdd';
|
|||||||
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
|
import WorkflowJobTemplateAdd from './WorkflowJobTemplateAdd';
|
||||||
|
|
||||||
function Templates({ i18n }) {
|
function Templates({ i18n }) {
|
||||||
const initBreadcrumbs = useRef({
|
const initScreenHeader = useRef({
|
||||||
'/templates': i18n._(t`Templates`),
|
'/templates': i18n._(t`Templates`),
|
||||||
'/templates/job_template/add': i18n._(t`Create New Job Template`),
|
'/templates/job_template/add': i18n._(t`Create New Job Template`),
|
||||||
'/templates/workflow_job_template/add': i18n._(
|
'/templates/workflow_job_template/add': i18n._(
|
||||||
t`Create New Workflow Template`
|
t`Create New Workflow Template`
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
const [breadcrumbConfig, setBreadcrumbs] = useState(initBreadcrumbs.current);
|
const [breadcrumbConfig, setScreenHeader] = useState(
|
||||||
|
initScreenHeader.current
|
||||||
|
);
|
||||||
|
|
||||||
|
const [schedule, setSchedule] = useState();
|
||||||
|
const [template, setTemplate] = useState();
|
||||||
|
|
||||||
const setBreadcrumbConfig = useCallback(
|
const setBreadcrumbConfig = useCallback(
|
||||||
(template, schedule) => {
|
(passedTemplate, passedSchedule) => {
|
||||||
|
if (passedTemplate && passedTemplate.name !== template?.name) {
|
||||||
|
setTemplate(passedTemplate);
|
||||||
|
}
|
||||||
|
if (passedSchedule && passedSchedule.name !== schedule?.name) {
|
||||||
|
setSchedule(passedSchedule);
|
||||||
|
}
|
||||||
if (!template) return;
|
if (!template) return;
|
||||||
const templatePath = `/templates/${template.type}/${template.id}`;
|
const templatePath = `/templates/${template.type}/${template.id}`;
|
||||||
const schedulesPath = `${templatePath}/schedules`;
|
const schedulesPath = `${templatePath}/schedules`;
|
||||||
const surveyPath = `${templatePath}/survey`;
|
const surveyPath = `${templatePath}/survey`;
|
||||||
setBreadcrumbs({
|
setScreenHeader({
|
||||||
...initBreadcrumbs.current,
|
...initScreenHeader.current,
|
||||||
[templatePath]: `${template.name}`,
|
[templatePath]: `${template.name}`,
|
||||||
[`${templatePath}/details`]: i18n._(t`Details`),
|
[`${templatePath}/details`]: i18n._(t`Details`),
|
||||||
[`${templatePath}/edit`]: i18n._(t`Edit Details`),
|
[`${templatePath}/edit`]: i18n._(t`Edit Details`),
|
||||||
@@ -40,16 +52,21 @@ function Templates({ i18n }) {
|
|||||||
[schedulesPath]: i18n._(t`Schedules`),
|
[schedulesPath]: i18n._(t`Schedules`),
|
||||||
[`${schedulesPath}/add`]: i18n._(t`Create New Schedule`),
|
[`${schedulesPath}/add`]: i18n._(t`Create New Schedule`),
|
||||||
[`${schedulesPath}/${schedule?.id}`]: `${schedule?.name}`,
|
[`${schedulesPath}/${schedule?.id}`]: `${schedule?.name}`,
|
||||||
[`${schedulesPath}/details`]: i18n._(t`Schedule Details`),
|
[`${schedulesPath}/${schedule?.id}/details`]: i18n._(
|
||||||
[`${schedulesPath}/edit`]: i18n._(t`Edit Details`),
|
t`Schedule Details`
|
||||||
|
),
|
||||||
|
[`${schedulesPath}/${schedule?.id}/edit`]: i18n._(t`Edit Schedule`),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[i18n]
|
[i18n, template, schedule]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="job_template,workflow_job_template,workflow_job_template_node"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/templates/job_template/add">
|
<Route path="/templates/job_template/add">
|
||||||
<JobTemplateAdd />
|
<JobTemplateAdd />
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import Templates from './Templates';
|
import Templates from './Templates';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Templates />', () => {
|
describe('<Templates />', () => {
|
||||||
let pageWrapper;
|
let pageWrapper;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Route, useRouteMatch, Switch } from 'react-router-dom';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
|
|
||||||
import UsersList from './UserList/UserList';
|
import UsersList from './UserList/UserList';
|
||||||
@@ -46,7 +46,7 @@ function Users({ i18n }) {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader streamType="user" breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.path}/add`}>
|
<Route path={`${match.path}/add`}>
|
||||||
<UserAdd />
|
<UserAdd />
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
|
|
||||||
import Users from './Users';
|
import Users from './Users';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<Users />', () => {
|
describe('<Users />', () => {
|
||||||
test('initially renders successfully', () => {
|
test('initially renders successfully', () => {
|
||||||
mountWithContexts(<Users />);
|
mountWithContexts(<Users />);
|
||||||
@@ -27,7 +31,7 @@ describe('<Users />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find('BreadcrumbHeading').length).toBe(1);
|
expect(wrapper.find('Title').length).toBe(1);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import WorkflowApprovalList from './WorkflowApprovalList';
|
import WorkflowApprovalList from './WorkflowApprovalList';
|
||||||
import WorkflowApproval from './WorkflowApproval';
|
import WorkflowApproval from './WorkflowApproval';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import ScreenHeader from '../../components/ScreenHeader/ScreenHeader';
|
||||||
|
|
||||||
function WorkflowApprovals({ i18n }) {
|
function WorkflowApprovals({ i18n }) {
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
@@ -26,7 +26,10 @@ function WorkflowApprovals({ i18n }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<ScreenHeader
|
||||||
|
streamType="workflow_approval"
|
||||||
|
breadcrumbConfig={breadcrumbConfig}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.url}/:id`}>
|
<Route path={`${match.url}/:id`}>
|
||||||
<WorkflowApproval setBreadcrumb={updateBreadcrumbConfig} />
|
<WorkflowApproval setBreadcrumb={updateBreadcrumbConfig} />
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { createMemoryHistory } from 'history';
|
|||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
import WorkflowApprovals from './WorkflowApprovals';
|
import WorkflowApprovals from './WorkflowApprovals';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<WorkflowApprovals />', () => {
|
describe('<WorkflowApprovals />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(<WorkflowApprovals />);
|
mountWithContexts(<WorkflowApprovals />);
|
||||||
@@ -29,7 +33,8 @@ describe('<WorkflowApprovals />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find('BreadcrumbHeading').length).toBe(1);
|
|
||||||
|
expect(wrapper.find('Title').length).toBe(1);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -118,15 +118,20 @@ function encodeValue(key, value) {
|
|||||||
* removing defaults. Used to put into url bar after ui route
|
* removing defaults. Used to put into url bar after ui route
|
||||||
* @param {object} qs config object for namespacing params, filtering defaults
|
* @param {object} qs config object for namespacing params, filtering defaults
|
||||||
* @param {object} query param object
|
* @param {object} query param object
|
||||||
|
* @param {object} any non-namespaced params to append
|
||||||
* @return {string} url query string
|
* @return {string} url query string
|
||||||
*/
|
*/
|
||||||
export const encodeNonDefaultQueryString = (config, params) => {
|
export const encodeNonDefaultQueryString = (
|
||||||
|
config,
|
||||||
|
params,
|
||||||
|
nonNamespacedParams = {}
|
||||||
|
) => {
|
||||||
if (!params) return '';
|
if (!params) return '';
|
||||||
|
|
||||||
const paramsWithoutDefaults = removeParams({}, params, config.defaultParams);
|
const paramsWithoutDefaults = removeParams({}, params, config.defaultParams);
|
||||||
return encodeQueryString(
|
return encodeQueryString({
|
||||||
namespaceParams(config.namespace, paramsWithoutDefaults)
|
...namespaceParams(config.namespace, paramsWithoutDefaults),
|
||||||
);
|
...nonNamespacedParams,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user