Skip to content

Commit

Permalink
Dashboard card showing metrics requiring action.
Browse files Browse the repository at this point in the history
Show a card in the dashboard with the number of metrics that require action (red, yellow, and white metrics). The card can be hidden via the Settings panel. Clicking the card or setting "Visible metrics" to "Metrics requiring actions" in the Settings panel hides metrics that do not require action.

Closes #8938.
  • Loading branch information
fniessink committed Jun 19, 2024
1 parent 53561ef commit f6bb45e
Show file tree
Hide file tree
Showing 36 changed files with 399 additions and 162 deletions.
5 changes: 5 additions & 0 deletions components/frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@
html {
scroll-padding-top: 163px; /* height of sticky header */
}

:root {
--inverted-menu-background-color: #1b1c1d;
--selection-color: #2185d0;
}
24 changes: 24 additions & 0 deletions components/frontend/src/dashboard/FilterCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { bool, func } from "prop-types"

import { Card } from "../semantic_ui_react_wrappers"
import { childrenPropType } from "../sharedPropTypes"

export function FilterCard({ children, onClick, selected }) {
return (
<Card
className="filter"
color={selected ? "blue" : null}
onClick={onClick}
onKeyPress={onClick}
style={{ height: "100%" }}
tabIndex="0"
>
{children}
</Card>
)
}
FilterCard.propTypes = {
children: childrenPropType,
onClick: func,
selected: bool,
}
16 changes: 16 additions & 0 deletions components/frontend/src/dashboard/FilterCardWithTable.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.ui.card.filter .table {
margin-top: 0.1em;
overflow: hidden;
white-space: nowrap;
table-layout: fixed;
}

.ui.card.filter .header {
margin-bottom: 0.3em;
overflow: hidden;
white-space: nowrap;
}

.ui.card.filter .header.selected {
color: var(--selection-color);
}
28 changes: 28 additions & 0 deletions components/frontend/src/dashboard/FilterCardWithTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import "./FilterCardWithTable.css"

import { bool, func, string } from "prop-types"

import { Card, Header, Table } from "../semantic_ui_react_wrappers"
import { childrenPropType } from "../sharedPropTypes"
import { FilterCard } from "./FilterCard"

export function FilterCardWithTable({ children, onClick, selected, title }) {
return (
<FilterCard onClick={onClick} selected={selected}>
<Card.Content>
<Header as="h3" className={selected ? "selected" : null} textAlign="center">
{title}
</Header>
<Table basic="very" compact="very" size="small">
<Table.Body>{children}</Table.Body>
</Table>
</Card.Content>
</FilterCard>
)
}
FilterCardWithTable.propTypes = {
children: childrenPropType,
onClick: func,
selected: bool,
title: string,
}
11 changes: 0 additions & 11 deletions components/frontend/src/dashboard/IssuesCard.css

This file was deleted.

29 changes: 13 additions & 16 deletions components/frontend/src/dashboard/IssuesCard.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import "./IssuesCard.css"

import { bool, func } from "prop-types"

import { Card, Header, Label, Table } from "../semantic_ui_react_wrappers"
import { Label, Table } from "../semantic_ui_react_wrappers"
import { reportPropType } from "../sharedPropTypes"
import { capitalize, ISSUE_STATUS_COLORS } from "../utils"
import { FilterCardWithTable } from "./FilterCardWithTable"

function issueStatuses(report) {
// The issue status is unknown when the issue was added recently and the status hasn't been collected yet
Expand All @@ -30,9 +29,9 @@ issueStatuses.propTypes = {
report: reportPropType,
}

export function IssuesCard({ onClick, report, selected }) {
function tableRows(report) {
const statuses = issueStatuses(report)
const tableRows = Object.keys(statuses).map((status) => (
return Object.keys(statuses).map((status) => (
<Table.Row key={status}>
<Table.Cell>{capitalize(status)}</Table.Cell>
<Table.Cell textAlign="right">
Expand All @@ -42,18 +41,16 @@ export function IssuesCard({ onClick, report, selected }) {
</Table.Cell>
</Table.Row>
))
const color = selected ? "blue" : null
}
tableRows.propTypes = {
report: reportPropType,
}

export function IssuesCard({ onClick, report, selected }) {
return (
<Card className="issues" color={color} onClick={onClick} onKeyPress={onClick} tabIndex="0">
<Card.Content>
<Header as="h3" color={color} textAlign="center">
{"Issues"}
</Header>
<Table basic="very" compact="very" size="small">
<Table.Body>{tableRows}</Table.Body>
</Table>
</Card.Content>
</Card>
<FilterCardWithTable onClick={onClick} selected={selected} title="Issues">
{tableRows(report)}
</FilterCardWithTable>
)
}
IssuesCard.propTypes = {
Expand Down
4 changes: 2 additions & 2 deletions components/frontend/src/dashboard/IssuesCard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ it("shows the correct title", () => {
expect(screen.getByText(/Issues/)).toBeInTheDocument()
})

it("shows the title in blue when selected", () => {
it("shows the title as selected when the card is selected", () => {
renderIssuesCard({ selected: true })
expect(screen.getByText(/Issues/)).toHaveClass("blue")
expect(screen.getByText(/Issues/)).toHaveClass("selected")
})

it("shows the number of issues", () => {
Expand Down
4 changes: 2 additions & 2 deletions components/frontend/src/dashboard/LegendCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { List } from "semantic-ui-react"

import { DarkMode } from "../context/DarkMode"
import { StatusIcon } from "../measurement/StatusIcon"
import { STATUS_SHORT_NAME, STATUSES } from "../metric/status"
import { Card } from "../semantic_ui_react_wrappers"
import { getStatusName, STATUSES } from "../utils"

export function LegendCard() {
const darkMode = useContext(DarkMode)
Expand All @@ -17,7 +17,7 @@ export function LegendCard() {
<StatusIcon status={status} size="small" />
</List.Icon>
<List.Content verticalAlign="middle" style={{ color: color }}>
{getStatusName(status)}
{STATUS_SHORT_NAME[status]}
</List.Content>
</List.Item>
))
Expand Down
11 changes: 6 additions & 5 deletions components/frontend/src/dashboard/MetricSummaryCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { VictoryContainer, VictoryLabel, VictoryPortal, VictoryTooltip } from "v

import { DarkMode } from "../context/DarkMode"
import { useBoundingBox } from "../hooks/boundingbox"
import { STATUS_COLORS_RGB, STATUSES } from "../metric/status"
import { Card } from "../semantic_ui_react_wrappers"
import { pluralize, STATUS_COLORS_RGB, STATUSES, sum } from "../utils"
import { pluralize, sum } from "../utils"
import { FilterCard } from "./FilterCard"
import { StatusBarChart } from "./StatusBarChart"
import { StatusPieChart } from "./StatusPieChart"

Expand Down Expand Up @@ -87,10 +89,9 @@ export function MetricSummaryCard({ header, onClick, selected, summary, maxY })
tooltip: tooltip,
width: Math.max(bbWidth, 1), // Prevent "Failed prop type: Invalid prop range supplied to VictoryBar"
}
const color = selected ? "blue" : null
return (
<Card color={color} style={{ height: "100%" }} onClick={onClick} onKeyPress={onClick} tabIndex="0">
<div ref={ref} style={{ width: "100%", height: "72%" }} aria-label={ariaChartLabel(summary)}>
<FilterCard onClick={onClick} selected={selected}>
<div ref={ref} style={{ height: "72%" }} aria-label={ariaChartLabel(summary)}>
<VictoryContainer width={bbWidth} height={bbHeight}>
{dates.length > 1 ? (
<StatusBarChart summary={summary} nrdates={dates.length} {...chartProps} />
Expand All @@ -102,7 +103,7 @@ export function MetricSummaryCard({ header, onClick, selected, summary, maxY })
<Card.Content>
<Card.Header textAlign="center">{header}</Card.Header>
</Card.Content>
</Card>
</FilterCard>
)
}
MetricSummaryCard.propTypes = {
Expand Down
71 changes: 71 additions & 0 deletions components/frontend/src/dashboard/MetricsRequiringActionCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { bool, func } from "prop-types"

import { STATUS_COLORS, STATUS_NAME, STATUSES_REQUIRING_ACTION } from "../metric/status"
import { Label, Table } from "../semantic_ui_react_wrappers"
import { reportsPropType } from "../sharedPropTypes"
import { getMetricStatus, sum } from "../utils"
import { FilterCardWithTable } from "./FilterCardWithTable"

function metricStatuses(reports) {
const statuses = {}
STATUSES_REQUIRING_ACTION.forEach((status) => {
statuses[status] = 0
})
reports.forEach((report) => {
Object.values(report.subjects).forEach((subject) => {
Object.values(subject.metrics).forEach((metric) => {
const status = getMetricStatus(metric)
if (STATUSES_REQUIRING_ACTION.includes(status)) {
statuses[status] += 1
}
})
})
})
return statuses
}
metricStatuses.propTypes = {
reports: reportsPropType,
}

function tableRows(reports) {
const statuses = metricStatuses(reports)
const rows = Object.keys(statuses).map((status) => (
<Table.Row key={status}>
<Table.Cell>{STATUS_NAME[status]}</Table.Cell>
<Table.Cell textAlign="right">
<Label size="small" color={STATUS_COLORS[status] === "white" ? null : STATUS_COLORS[status]}>
{statuses[status]}
</Label>
</Table.Cell>
</Table.Row>
))
rows.push(
<Table.Row key="total">
<Table.Cell>
<b>Total</b>
</Table.Cell>
<Table.Cell textAlign="right">
<Label size="small" color="black">
{sum(Object.values(statuses))}
</Label>
</Table.Cell>
</Table.Row>,
)
return rows
}
tableRows.propTypes = {
reports: reportsPropType,
}

export function MetricsRequiringActionCard({ onClick, reports, selected }) {
return (
<FilterCardWithTable onClick={onClick} selected={selected} title="Action required">
{tableRows(reports)}
</FilterCardWithTable>
)
}
MetricsRequiringActionCard.propTypes = {
onClick: func,
reports: reportsPropType,
selected: bool,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { render, screen } from "@testing-library/react"

import { MetricsRequiringActionCard } from "./MetricsRequiringActionCard"

const report = {
subjects: {
subject_uuid: {
metrics: {
metric_uuid: {
status: "target_not_met",
},
another_metric_uuid: {
status: "near_target_met",
},
},
},
another_subject_uuid: {
metrics: {
yet_another_metric_uuid: {
status: "near_target_met",
},
},
},
},
}

function renderMetricsRequiringActionCard({ selected = false } = {}) {
render(<MetricsRequiringActionCard reports={[report]} selected={selected} />)
}

it("shows the correct title", () => {
renderMetricsRequiringActionCard()
expect(screen.getByText(/Action required/)).toBeInTheDocument()
})

it("shows the title as selected when the card is selected", () => {
renderMetricsRequiringActionCard({ selected: true })
expect(screen.getByText(/Action required/)).toHaveClass("selected")
})

it("shows the number of metrics", () => {
renderMetricsRequiringActionCard()
expect(screen.getByRole("row", { name: "Unknown 0" })).toBeInTheDocument()
expect(screen.getByRole("row", { name: "Target not met 1" })).toBeInTheDocument()
expect(screen.getByRole("row", { name: "Near target met 2" })).toBeInTheDocument()
expect(screen.getByRole("row", { name: "Total 3" })).toBeInTheDocument()
})
3 changes: 2 additions & 1 deletion components/frontend/src/dashboard/StatusBarChart.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { number, object } from "prop-types"
import { VictoryBar, VictoryStack } from "victory"

import { STATUS_COLORS, STATUS_NAME, STATUSES } from "../metric/status"
import { labelPropType, stringsPropType } from "../sharedPropTypes"
import { pluralize, STATUS_COLORS, STATUS_NAME, STATUSES, sum } from "../utils"
import { pluralize, sum } from "../utils"

function nrMetricsLabel(nrMetrics) {
return nrMetrics === 0 ? "No metrics" : nrMetrics + pluralize(" metric", nrMetrics)
Expand Down
3 changes: 2 additions & 1 deletion components/frontend/src/dashboard/StatusPieChart.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { element, number, object } from "prop-types"
import { VictoryPie } from "victory"

import { STATUS_COLORS, STATUS_NAME, STATUSES } from "../metric/status"
import { stringsPropType } from "../sharedPropTypes"
import { pluralize, STATUS_COLORS, STATUS_NAME, STATUSES, sum } from "../utils"
import { pluralize, sum } from "../utils"

function nrMetricsLabel(nrMetrics) {
return nrMetrics === 0 ? "No metrics" : nrMetrics + pluralize(" metric", nrMetrics)
Expand Down
8 changes: 7 additions & 1 deletion components/frontend/src/header_footer/Footer.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
@media print {
#Footer {
div.footer {
display: none;
}
}

div.footer {
background-color: var(--inverted-menu-background-color) !important;
margin: 5em 0em 0em !important;
padding: 5em 0em 3em !important;
}
6 changes: 1 addition & 5 deletions components/frontend/src/header_footer/Footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,7 @@ function QuoteColumn() {

export function Footer({ lastUpdate, report }) {
return (
<Segment
inverted
id="Footer"
style={{ margin: "5em 0em 0em", padding: "5em 0em 3em", backgroundColor: "#1b1c1d" }}
>
<Segment className="footer" inverted>
<Container>
<Grid>
<Grid.Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function renderResetSettingsButton({
it("resets the settings", async () => {
history.push(
"?date_interval=2&date_order=ascending&hidden_columns=comment&hidden_tags=tag&" +
"metrics_to_hide=no_action_needed&nr_dates=2&show_issue_creation_date=true&show_issue_summary=true&" +
"metrics_to_hide=no_action_required&nr_dates=2&show_issue_creation_date=true&show_issue_summary=true&" +
"show_issue_update_date=true&show_issue_due_date=true&show_issue_release=true&show_issue_sprint=true&" +
"sort_column=status&sort_direction=descending&expanded=tab:0&hidden_cards=tags",
)
Expand Down
Loading

0 comments on commit f6bb45e

Please sign in to comment.