Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
110 changes: 54 additions & 56 deletions cypress/e2e/awx/general-ui/dashboard-checks.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AwxItemsResponse } from '../../../../frontend/awx/common/AwxItemsResponse';
import { IAwxDashboardData } from '../../../../frontend/awx/dashboard/AwxDashboard';
import { Inventory } from '../../../../frontend/awx/interfaces/Inventory';
import { Job } from '../../../../frontend/awx/interfaces/Job';
import { Project } from '../../../../frontend/awx/interfaces/Project';
Expand Down Expand Up @@ -30,33 +31,34 @@ describe('Dashboard: General UI tests - resources count and empty state check',
cy.visit(`/ui_next/dashboard`);
cy.clickButton('Manage view');
cy.get('.pf-c-modal-box__title-text').should('contain', 'Manage Dashboard');
cy.contains('tr', 'Projects').find('input').uncheck();
cy.contains('tr', 'Resource Counts').find('input').uncheck();
cy.clickModalButton('Apply');
cy.contains('.pf-c-card__header', 'Projects').should('not.be.visible');
cy.contains('.pf-c-title', 'Hosts').should('not.exist');
cy.clickButton('Manage view');
cy.get('.pf-c-modal-box__title-text').should('contain', 'Manage Dashboard');
cy.contains('tr', 'Projects').find('input').check();
cy.contains('tr', 'Resource Counts').find('input').check();
cy.clickModalButton('Apply');
cy.contains('.pf-c-card__header', 'Projects').should('be.visible');
cy.contains('.pf-c-title', 'Hosts').should('be.visible');
});

it('within the Manage Dashboard modal, clicking the Cancel button should revert any changes', () => {
cy.visit(`/ui_next/dashboard`);
cy.clickButton('Manage view');
cy.get('.pf-c-modal-box__title-text').should('contain', 'Manage Dashboard');
cy.contains('tr', 'Projects').find('input').uncheck();
cy.contains('tr', 'Resource Counts').find('input').uncheck();
cy.clickModalButton('Cancel');
cy.contains('.pf-c-card__header', 'Projects').should('be.visible');
cy.contains('.pf-c-title', 'Hosts').should('be.visible');
});

it('within the Manage Dashboard modal, clicking the Close button should revert any changes', () => {
cy.visit(`/ui_next/dashboard`);
cy.clickButton('Manage view');
cy.get('.pf-c-modal-box__title-text').should('contain', 'Manage Dashboard');
cy.contains('tr', 'Projects').find('input').uncheck();
cy.contains('tr', 'Resource Counts').find('input').uncheck();
cy.get('[aria-label="Close"]').click();
cy.contains('.pf-c-card__header', 'Projects').should('be.visible');
cy.contains('.pf-c-title', 'Hosts').should('be.visible');
});

// Manage Dashboard modal table does not currently support keyboard input to reorder items, use drag & drop
it('within the Manage Dashboard modal, dragging a resource should reorder the resource', () => {
let initialArray: string[];
Expand All @@ -67,7 +69,7 @@ describe('Dashboard: General UI tests - resources count and empty state check',
initialArray = Array.from(headers, (title) => title.innerText.split('\n')[0]);
cy.clickButton('Manage view');
cy.get('.pf-c-modal-box__title-text').should('contain', 'Manage Dashboard');
cy.get('#draggable-row-project').drag('#draggable-row-recent_job_activity');
cy.get('#draggable-row-recent_jobs').drag('#draggable-row-recent_job_activity');
cy.clickModalButton('Apply');
});
cy.get('.pf-c-card__header').then((headers) => {
Expand All @@ -77,62 +79,58 @@ describe('Dashboard: General UI tests - resources count and empty state check',
});

it('checks inventories count', () => {
cy.intercept('GET', 'api/v2/dashboard/').as('getInventories');
cy.intercept('GET', 'api/v2/dashboard/').as('getDashboard');
cy.visit(`/ui_next/dashboard`);
cy.contains('.pf-c-card__header', 'Inventories')
.next()
.within(() => {
cy.contains('tspan', 'Ready')
.invoke('text')
.then((text: string) => {
cy.wait('@getInventories')
.its('response.body.inventories.total')
.then((total) => {
expect(total).to.equal(parseInt(text.split(':')[1]));
});
});
cy.wait('@getDashboard')
.its('response.body')
.then((data: IAwxDashboardData) => {
cy.get('#inventories-chart').should('contain', data.inventories.total);
const readyCount = data.inventories.total - data.inventories.inventory_failed;
if (readyCount > 0) {
cy.get('#inventories-legend-synced-count').should('contain', readyCount);
}
if (data.inventories.inventory_failed > 0) {
cy.get('#inventories-legend-failed-count').should(
'contain',
data.inventories.inventory_failed
);
}
});
cy.checkAnchorLinks('Go to Inventories');
});

it('checks hosts count', () => {
cy.intercept('GET', 'api/v2/dashboard/').as('getHosts');
cy.intercept('GET', 'api/v2/dashboard/').as('getDashboard');
cy.visit(`/ui_next/dashboard`);
cy.contains('.pf-c-card__header', 'Hosts')
.next()
.within(() => {
cy.contains('tspan', 'Ready')
.invoke('text')
.then((text: string) => {
cy.wait('@getHosts')
.its('response.body.hosts.total')
.then((total) => {
expect(total).to.equal(parseInt(text.split(':')[1]));
});
});
cy.wait('@getDashboard')
.its('response.body')
.then((data: IAwxDashboardData) => {
cy.get('#hosts-chart').should('contain', data.hosts.total);
const readyCount = data.hosts.total - data.hosts.failed;
if (readyCount > 0) {
cy.get('#hosts-legend-ready-count').should('contain', readyCount);
}
if (data.hosts.failed > 0) {
cy.get('#hosts-legend-failed-count').should('contain', data.hosts.failed);
}
});
cy.checkAnchorLinks('Go to Hosts');
});

// JT Disabling invalid test. Ready count does not always match the total count.
// it('checks projects count', () => {
// cy.intercept('GET', 'api/v2/dashboard/').as('getProjects');
// cy.visit(`/ui_next/dashboard`);
// cy.contains('.pf-c-card__header', 'Projects')
// .next()
// .within(() => {
// cy.contains('tspan', 'Ready')
// .invoke('text')
// .then((text: string) => {
// cy.wait('@getProjects')
// .its('response.body.projects.total')
// .then((total) => {
// expect(total).to.equal(parseInt(text.split(':')[1]));
// });
// });
// });
// cy.checkAnchorLinks('Go to Projects');
// });
it('checks projects count', () => {
cy.intercept('GET', 'api/v2/dashboard/').as('getDashboard');
cy.visit(`/ui_next/dashboard`);
cy.wait('@getDashboard')
.its('response.body')
.then((data: IAwxDashboardData) => {
cy.get('#projects-chart').should('contain', data.projects.total);
const readyCount = data.projects.total - data.projects.failed;
if (readyCount > 0) {
cy.get('#projects-legend-ready-count').should('contain', readyCount);
}
if (data.projects.failed > 0) {
cy.get('#projects-legend-failed-count').should('contain', data.projects.failed);
}
});
});

it('checks jobs count and the max # of jobs in the table', () => {
cy.intercept('GET', '/api/v2/unified_jobs/?order_by=-finished&page=1&page_size=10').as(
Expand Down
3 changes: 3 additions & 0 deletions cypress/support/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
// ***********************************************************

import '@patternfly/patternfly/patternfly-base.css';
import '@patternfly/patternfly/patternfly-charts.css';

import '@patternfly/patternfly/patternfly-charts-theme-dark.css';

import { Page } from '@patternfly/react-core';
import 'cypress-react-selector';
Expand Down
166 changes: 138 additions & 28 deletions framework/PageDashboard/PageDashboardCountBar.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,159 @@
import { CardBody, Flex, FlexItem, Title } from '@patternfly/react-core';
import { ArrowRightIcon } from '@patternfly/react-icons';
import { ChartDonut, ChartLabel, ChartLabelProps } from '@patternfly/react-charts';
import { CardBody, Title } from '@patternfly/react-core';
import { CSSProperties } from 'react';
import { Link } from 'react-router-dom';
import { PageDashboardCard } from './PageDashboardCard';

export type PageDashboardCountBarProps = {
counts: {
title: string;
count: number;
total?: number;
to: string;
counts?: { label: string; count: number; color: string; link?: string }[];
}[];
};

export function PageDashboardCountBar(props: PageDashboardCountBarProps) {
return (
<PageDashboardCard width="xxl" isCompact>
<CardBody>
<Flex
spaceItems={{ default: 'spaceItems2xl' }}
style={{ rowGap: 16 }}
justifyContent={{ default: 'justifyContentSpaceEvenly' }}
<div
style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-evenly',
paddingLeft: 8,
paddingRight: 8,
gap: 16,
}}
>
{props.counts.map((item) => (
<FlexItem key={item.title}>
<Flex
alignItems={{ default: 'alignItemsCenter' }}
spaceItems={{ default: 'spaceItemsMd' }}
>
<FlexItem>
<Title headingLevel="h3" size="xl">
{props.counts.map((item, index) => {
const id = item.title.toLowerCase().replace(/ /g, '-');
const total: number =
item.total ??
item.counts?.reduce<number>((acc: number, curr) => acc + (curr.count ?? 0), 0) ??
0;
return (
<div id={id} key={index} style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Link to={item.to} style={{ color: 'var(--pf-global--text--Color)' }}>
<Title
headingLevel="h3"
size="xl"
style={{ whiteSpace: 'nowrap', textDecoration: 'none' }}
>
{item.title}
</Title>
</FlexItem>
<FlexItem>
<span style={{ fontSize: 'xx-large', lineHeight: 1 }}>{item.count}</span>
</FlexItem>
<FlexItem>
<Link to={item.to}>
<ArrowRightIcon />
</Link>
</FlexItem>
</Flex>
</FlexItem>
))}
</Flex>
</Link>

{/* if there is a total and the item has counts and counts is not undefined */}
{/* then we want to show the donut chart instead of just a count whisch is in the else */}
{/* Note: even if there is counts but total is 0 we want to just render the count */}
{total && 'counts' in item && item.counts ? (
<>
<div
id={`${id}-chart`}
style={{ maxHeight: 64, marginTop: -4, marginBottom: -4 }}
>
<ChartDonut
title={item.counts
.reduce<number>((acc, curr) => acc + curr.count, 0)
.toString()}
titleComponent={<PageChartLabel to={item.to} />}
padding={{ top: 0, left: 0, right: 0, bottom: 0 }}
width={64}
height={64}
data={item.counts.map((count) => ({ x: count.label, y: count.count }))}
colorScale={item.counts.map((count) => count.color)}
cornerRadius={3}
allowTooltip={false}
/>
</div>
<PageChartLegend id={`${id}-legend`} legend={item.counts} />
</>
) : (
// This renders the total if there are no counts or total is zero
<span style={{ fontSize: 'xx-large', lineHeight: 1 }}>{total}</span>
)}
</div>
);
})}
</div>
</CardBody>
</PageDashboardCard>
);
}

export function PageChartLegend(props: {
id: string;
legend: { label: string; count: number; color: string; link?: string }[];
}) {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'auto auto auto',
columnGap: 6,
alignItems: 'center',
}}
>
{props.legend.map((item, index) => {
if (item.count === 0) return <></>;
return (
<>
<div
key={index * 3 + 0}
style={{ width: 10, height: 10, backgroundColor: item.color, borderRadius: 2 }}
/>
<div
id={`${props.id}-${item.label.toLowerCase().replace(/ /g, '-')}-count`}
key={index * 3 + 1}
style={{ fontSize: 'small', textAlign: 'center' }}
>
{item.count}
</div>
<div key={index * 3 + 2} style={{ fontSize: 'small' }}>
{item.link ? (
<Link to={item.link} style={{ textDecoration: 'none' }}>
{item.label}
</Link>
) : (
item.label
)}
</div>
</>
);
})}
</div>
);
}

function PageChartLabel(props: ChartLabelProps & { to?: string }) {
if (props.to) {
return (
<Link to={props.to} style={{ textDecoration: 'none' }}>
<ChartLabel
{...props}
style={
{
...props.style,
fill: 'var(--pf-global--link--Color)',
stroke: 'var(--pf-global--link--Color)',
} as CSSProperties
}
/>
</Link>
);
}
return (
<ChartLabel
{...props}
style={
{
...props.style,
fill: 'var(--pf-global--text--Color)',
stroke: 'var(--pf-global--text--Color)',
} as CSSProperties
}
/>
);
}
Loading