Skip to content

Commit

Permalink
Merge pull request #8402 from jlmitch5/dashboardAug20
Browse files Browse the repository at this point in the history
  • Loading branch information
softwarefactory-project-zuul[bot] authored Oct 22, 2020
2 parents a532421 + e30569c commit 466dff9
Show file tree
Hide file tree
Showing 19 changed files with 2,126 additions and 37 deletions.
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':
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 id="dashboard-main-container">
<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 dashboard graph by default', () => {
expect(pageWrapper.find('LineChart').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

0 comments on commit 466dff9

Please sign in to comment.