Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ui_next dashboard #8402

Merged
merged 8 commits into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion awx/api/templates/api/dashboard_jobs_graph_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The `period` of the data can be adjusted with:

?period=month

Where `month` can be replaced with `week`, or `day`. `month` is the default.
Where `month` can be replaced with `week`, `two_weeks`, or `day`. `month` is the default.

The type of job can be filtered with:

Expand Down
3 changes: 3 additions & 0 deletions awx/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ def get(self, request, format=None):
if period == 'month':
end_date = start_date - dateutil.relativedelta.relativedelta(months=1)
interval = 'days'
elif period == 'two_weeks':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanpetrello this look good to you? Just want to make sure we're not sneaking something by y'all without a review.

end_date = start_date - dateutil.relativedelta.relativedelta(weeks=2)
interval = 'days'
elif period == 'week':
end_date = start_date - dateutil.relativedelta.relativedelta(weeks=1)
interval = 'days'
Expand Down
3 changes: 3 additions & 0 deletions awx/ui_next/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Config from './models/Config';
import CredentialInputSources from './models/CredentialInputSources';
import CredentialTypes from './models/CredentialTypes';
import Credentials from './models/Credentials';
import Dashboard from './models/Dashboard';
import Groups from './models/Groups';
import Hosts from './models/Hosts';
import InstanceGroups from './models/InstanceGroups';
Expand Down Expand Up @@ -42,6 +43,7 @@ const ConfigAPI = new Config();
const CredentialInputSourcesAPI = new CredentialInputSources();
const CredentialTypesAPI = new CredentialTypes();
const CredentialsAPI = new Credentials();
const DashboardAPI = new Dashboard();
const GroupsAPI = new Groups();
const HostsAPI = new Hosts();
const InstanceGroupsAPI = new InstanceGroups();
Expand Down Expand Up @@ -81,6 +83,7 @@ export {
CredentialInputSourcesAPI,
CredentialTypesAPI,
CredentialsAPI,
DashboardAPI,
GroupsAPI,
HostsAPI,
InstanceGroupsAPI,
Expand Down
16 changes: 16 additions & 0 deletions awx/ui_next/src/api/models/Dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Base from '../Base';

class Dashboard extends Base {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/dashboard/';
}

readJobGraph(params) {
return this.http.get(`${this.baseUrl}graphs/jobs/`, {
params,
});
}
}

export default Dashboard;
257 changes: 241 additions & 16 deletions awx/ui_next/src/screens/Dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,253 @@
import React, { Component, Fragment } from 'react';
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Card,
CardHeader,
CardActions,
CardBody,
PageSection,
PageSectionVariants,
Select,
SelectVariant,
SelectOption,
Tabs,
Tab,
TabTitleText,
Title,
} from '@patternfly/react-core';

class Dashboard extends Component {
render() {
const { i18n } = this.props;
const { light } = PageSectionVariants;

return (
<Fragment>
<PageSection variant={light} className="pf-m-condensed">
<Title size="2xl" headingLevel="h2">
{i18n._(t`Dashboard`)}
</Title>
</PageSection>
<PageSection />
</Fragment>
);
import useRequest from '../../util/useRequest';
import { DashboardAPI } from '../../api';
import JobList from '../../components/JobList';

import LineChart from './shared/LineChart';
import Count from './shared/Count';
import DashboardTemplateList from './shared/DashboardTemplateList';

const Counts = styled.div`
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-gap: var(--pf-global--spacer--lg);

@media (max-width: 900px) {
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
}
`;

const MainPageSection = styled(PageSection)`
padding-top: 0;
padding-bottom: 0;

& .spacer {
margin-bottom: var(--pf-global--spacer--lg);
}
`;

const GraphCardHeader = styled(CardHeader)`
margin-top: var(--pf-global--spacer--lg);
`;

const GraphCardActions = styled(CardActions)`
margin-left: initial;
padding-left: 0;
`;

function Dashboard({ i18n }) {
const { light } = PageSectionVariants;

const [isPeriodDropdownOpen, setIsPeriodDropdownOpen] = useState(false);
const [isJobTypeDropdownOpen, setIsJobTypeDropdownOpen] = useState(false);
const [periodSelection, setPeriodSelection] = useState('month');
const [jobTypeSelection, setJobTypeSelection] = useState('all');
const [activeTabId, setActiveTabId] = useState(0);

const {
result: { jobGraphData, countData },
request: fetchDashboardGraph,
} = useRequest(
useCallback(async () => {
const [{ data }, { data: dataFromCount }] = await Promise.all([
DashboardAPI.readJobGraph({
period: periodSelection,
job_type: jobTypeSelection,
}),
DashboardAPI.read(),
]);
const newData = {};
data.jobs.successful.forEach(([dateSecs, count]) => {
if (!newData[dateSecs]) {
newData[dateSecs] = {};
}
newData[dateSecs].successful = count;
});
data.jobs.failed.forEach(([dateSecs, count]) => {
if (!newData[dateSecs]) {
newData[dateSecs] = {};
}
newData[dateSecs].failed = count;
});
const jobData = Object.keys(newData).map(dateSecs => {
const [created] = new Date(dateSecs * 1000).toISOString().split('T');
newData[dateSecs].created = created;
return newData[dateSecs];
});
return {
jobGraphData: jobData,
countData: dataFromCount,
};
}, [periodSelection, jobTypeSelection]),
{
jobGraphData: [],
countData: {},
}
);

useEffect(() => {
fetchDashboardGraph();
}, [fetchDashboardGraph, periodSelection, jobTypeSelection]);

return (
<Fragment>
<PageSection variant={light} className="pf-m-condensed">
<Title size="2xl" headingLevel="h2">
{i18n._(t`Dashboard`)}
</Title>
</PageSection>
<PageSection>
<Counts>
<Count
link="/hosts"
data={countData?.hosts?.total}
label={i18n._(t`Hosts`)}
/>
<Count
failed
link="/hosts?host.last_job_host_summary__failed=true"
data={countData?.hosts?.failed}
label={i18n._(t`Failed hosts`)}
/>
<Count
link="/inventories"
data={countData?.inventories?.total}
label={i18n._(t`Inventories`)}
/>
<Count
failed
link="/inventories?inventory.inventory_sources_with_failures__gt=0"
data={countData?.inventories?.failed}
label={i18n._(t`Inventory sync failures`)}
/>
<Count
link="/projects"
data={countData?.projects?.total}
label={i18n._(t`Projects`)}
/>
<Count
failed
link="/projects?project.status__in=failed,canceled"
data={countData?.projects?.failed}
label={i18n._(t`Project sync failures`)}
/>
</Counts>
</PageSection>
<MainPageSection>
<div className="spacer">
<Card>
<Tabs
aria-label={i18n._(t`Tabs`)}
activeKey={activeTabId}
onSelect={(key, eventKey) => setActiveTabId(eventKey)}
>
<Tab
aria-label={i18n._(t`Job status graph tab`)}
eventKey={0}
title={<TabTitleText>{i18n._(t`Job status`)}</TabTitleText>}
/>
<Tab
aria-label={i18n._(t`Recent Jobs list tab`)}
eventKey={1}
title={<TabTitleText>{i18n._(t`Recent Jobs`)}</TabTitleText>}
/>
<Tab
aria-label={i18n._(t`Recent Templates list tab`)}
eventKey={2}
title={
<TabTitleText>{i18n._(t`Recent Templates`)}</TabTitleText>
}
/>
</Tabs>
{activeTabId === 0 && (
<Fragment>
<GraphCardHeader>
<GraphCardActions>
<Select
variant={SelectVariant.single}
placeholderText={i18n._(t`Select period`)}
aria-label={i18n._(t`Select period`)}
className="periodSelect"
onToggle={setIsPeriodDropdownOpen}
onSelect={(event, selection) =>
setPeriodSelection(selection)
}
selections={periodSelection}
isOpen={isPeriodDropdownOpen}
>
<SelectOption key="month" value="month">
{i18n._(t`Past month`)}
</SelectOption>
<SelectOption key="two_weeks" value="two_weeks">
{i18n._(t`Past two weeks`)}
</SelectOption>
<SelectOption key="week" value="week">
{i18n._(t`Past week`)}
</SelectOption>
</Select>
<Select
variant={SelectVariant.single}
placeholderText={i18n._(t`Select job type`)}
aria-label={i18n._(t`Select job type`)}
className="jobTypeSelect"
onToggle={setIsJobTypeDropdownOpen}
onSelect={(event, selection) =>
setJobTypeSelection(selection)
}
selections={jobTypeSelection}
isOpen={isJobTypeDropdownOpen}
>
<SelectOption key="all" value="all">
{i18n._(t`All job types`)}
</SelectOption>
<SelectOption key="inv_sync" value="inv_sync">
{i18n._(t`Inventory sync`)}
</SelectOption>
<SelectOption key="scm_update" value="scm_update">
{i18n._(t`SCM update`)}
</SelectOption>
<SelectOption key="playbook_run" value="playbook_run">
{i18n._(t`Playbook run`)}
</SelectOption>
</Select>
</GraphCardActions>
</GraphCardHeader>
<CardBody>
<LineChart
height={390}
id="d3-line-chart-root"
data={jobGraphData}
/>
</CardBody>
</Fragment>
)}
{activeTabId === 1 && <JobList defaultParams={{ page_size: 5 }} />}
{activeTabId === 2 && <DashboardTemplateList />}
</Card>
</div>
</MainPageSection>
</Fragment>
);
}

export default withI18n()(Dashboard);
41 changes: 31 additions & 10 deletions awx/ui_next/src/screens/Dashboard/Dashboard.test.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import React from 'react';
import { act } from 'react-dom/test-utils';

import { mountWithContexts } from '../../../testUtils/enzymeHelpers';

import { DashboardAPI } from '../../api';
import Dashboard from './Dashboard';

jest.mock('../../api');

describe('<Dashboard />', () => {
let pageWrapper;
let pageSections;
let title;
let graphRequest;

beforeEach(() => {
pageWrapper = mountWithContexts(<Dashboard />);
pageSections = pageWrapper.find('PageSection');
title = pageWrapper.find('Title');
beforeEach(async () => {
await act(async () => {
DashboardAPI.read.mockResolvedValue({});
graphRequest = DashboardAPI.readJobGraph;
graphRequest.mockResolvedValue({});
pageWrapper = mountWithContexts(<Dashboard />);
});
});

afterEach(() => {
Expand All @@ -21,9 +27,24 @@ describe('<Dashboard />', () => {

test('initially renders without crashing', () => {
expect(pageWrapper.length).toBe(1);
expect(pageSections.length).toBe(2);
expect(title.length).toBe(1);
expect(title.props().size).toBe('2xl');
expect(pageSections.first().props().variant).toBe('light');
});

test('renders jobs list by default', () => {
expect(pageWrapper.find('JobList').length).toBe(1);
});

test('renders template list when the active tab is changed', async () => {
expect(pageWrapper.find('DashboardTemplateList').length).toBe(0);
pageWrapper
.find('button[aria-label="Recent Templates list tab"]')
.simulate('click');
expect(pageWrapper.find('DashboardTemplateList').length).toBe(1);
});

test('renders month-based/all job type chart by default', () => {
expect(graphRequest).toHaveBeenCalledWith({
job_type: 'all',
period: 'month',
});
});
});
Loading