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 18, 2024
1 parent 3bd04fd commit b3c0b88
Show file tree
Hide file tree
Showing 20 changed files with 281 additions and 51 deletions.
16 changes: 16 additions & 0 deletions components/frontend/src/dashboard/FilterCard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.ui.card.filter_card .table {
margin-top: 0.1em;
overflow: hidden;
white-space: nowrap;
table-layout: fixed;
}

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

.ui.card.filter_card .header.blue {
color: #2185d0;
}
28 changes: 28 additions & 0 deletions components/frontend/src/dashboard/FilterCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import "./FilterCard.css"

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

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

export function FilterCard({ children, onClick, selected, title }) {
const color = selected ? "blue" : null
return (
<Card className="filter_card" color={color} onClick={onClick} onKeyPress={onClick} tabIndex="0">
<Card.Content>
<Header as="h3" color={color} textAlign="center">
{title}
</Header>
<Table basic="very" compact="very" size="small">
<Table.Body>{children}</Table.Body>
</Table>
</Card.Content>
</Card>
)
}
FilterCard.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 { FilterCard } from "./FilterCard"

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>
<FilterCard onClick={onClick} selected={selected} title="Issues">
{tableRows(report)}
</FilterCard>
)
}
IssuesCard.propTypes = {
Expand Down
70 changes: 70 additions & 0 deletions components/frontend/src/dashboard/MetricsRequiringActionCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { bool, func } from "prop-types"

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

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 (
<FilterCard onClick={onClick} selected={selected} title="Action required">
{tableRows(reports)}
</FilterCard>
)
}
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 in blue when selected", () => {
renderMetricsRequiringActionCard({ selected: true })
expect(screen.getByText(/Action required/)).toHaveClass("blue")
})

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()
})
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
5 changes: 3 additions & 2 deletions components/frontend/src/header_footer/SettingsPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ export function SettingsPanel({ atReportsOverview, handleSort, settings, tags })
<VisibleCardsMenuItem cards={atReportsOverview ? "reports" : "subjects"} {...cardsMenuItemProps} />
<VisibleCardsMenuItem cards="tags" {...cardsMenuItemProps} />
<VisibleCardsMenuItem cards="issues" {...cardsMenuItemProps} />
<VisibleCardsMenuItem cards="action_required" {...cardsMenuItemProps} />
<VisibleCardsMenuItem cards="legend" {...cardsMenuItemProps} />
</Menu>
</Segment>
<Segment inverted color="black">
<Header size="small">Visible metrics</Header>
<Menu {...menuProps}>
<MetricMenuItem hide="none" {...metricMenuItemProps} />
<MetricMenuItem hide="no_action_needed" {...metricMenuItemProps} />
<MetricMenuItem hide="no_action_required" {...metricMenuItemProps} />
<MetricMenuItem hide="no_issues" {...metricMenuItemProps} />
<MetricMenuItem hide="all" {...metricMenuItemProps} />
</Menu>
Expand Down Expand Up @@ -381,7 +382,7 @@ function MetricMenuItem({ hide, metricsToHide }) {
{
{
none: "All metrics",
no_action_needed: "Metrics requiring action",
no_action_required: "Metrics requiring action",
no_issues: "Metrics with issues",
all: "No metrics",
}[hide]
Expand Down
6 changes: 3 additions & 3 deletions components/frontend/src/header_footer/SettingsPanel.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,18 @@ function renderSettingsPanel({
it("hides the metrics not requiring action", async () => {
renderSettingsPanel()
fireEvent.click(screen.getByText(/Metrics requiring action/))
expect(history.location.search).toBe("?metrics_to_hide=no_action_needed")
expect(history.location.search).toBe("?metrics_to_hide=no_action_required")
})

it("shows all metrics", async () => {
history.push("?metrics_to_hide=no_action_needed")
history.push("?metrics_to_hide=no_action_required")
renderSettingsPanel()
fireEvent.click(screen.getByText(/All metrics/))
expect(history.location.search).toBe("")
})

it("shows all metrics by keypress", async () => {
history.push("?metrics_to_hide=no_action_needed")
history.push("?metrics_to_hide=no_action_required")
renderSettingsPanel()
await userEvent.type(screen.getByText(/All metrics/), " ")
expect(history.location.search).toBe("")
Expand Down
14 changes: 14 additions & 0 deletions components/frontend/src/report/ReportDashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DataModel } from "../context/DataModel"
import { CardDashboard } from "../dashboard/CardDashboard"
import { IssuesCard } from "../dashboard/IssuesCard"
import { LegendCard } from "../dashboard/LegendCard"
import { MetricsRequiringActionCard } from "../dashboard/MetricsRequiringActionCard"
import { MetricSummaryCard } from "../dashboard/MetricSummaryCard"
import { datesPropType, measurementPropType, reportPropType, settingsPropType } from "../sharedPropTypes"
import {
Expand Down Expand Up @@ -86,6 +87,19 @@ export function ReportDashboard({ dates, measurements, onClick, onClickTag, relo
})
}
const extraCards = []
if (settings.hiddenCards.excludes("action_required")) {
const metric_requiring_action_selected = settings.metricsToHide.value === "no_action_required"
extraCards.push(
<MetricsRequiringActionCard
key="metrics_requiring_action"
reports={[report]}
onClick={() =>
settings.metricsToHide.set(metric_requiring_action_selected ? "none" : "no_action_required")
}
selected={metric_requiring_action_selected}
/>,
)
}
if (report.issue_tracker?.type && settings.hiddenCards.excludes("issues")) {
const selected = settings.metricsToHide.value === "no_issues"
extraCards.push(
Expand Down
26 changes: 25 additions & 1 deletion components/frontend/src/report/ReportDashboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ it("hides a subject if all its tags are hidden", async () => {
expect(screen.queryAllByText(/other/).length).toBe(0)
})

it("calls the callback on click", async () => {
it("expand the subject title on click", async () => {
const onClick = jest.fn()
renderDashboard({ reportToRender: report, onClick: onClick })
fireEvent.click(screen.getByText(/Subject title/))
Expand All @@ -87,6 +87,7 @@ it("hides the subject cards", async () => {
history.push("?hidden_cards=subjects")
renderDashboard({ reportToRender: report })
expect(screen.queryAllByText(/Subject title/).length).toBe(0)
expect(screen.getAllByText(/Action required/).length).toBe(1)
expect(screen.getAllByText(/tag/).length).toBe(1)
expect(screen.getAllByText(/other/).length).toBe(1)
})
Expand All @@ -95,10 +96,33 @@ it("hides the tag cards", async () => {
history.push("?hidden_cards=tags")
renderDashboard({ reportToRender: report })
expect(screen.getAllByText(/Subject title/).length).toBe(1)
expect(screen.getAllByText(/Action required/).length).toBe(1)
expect(screen.queryAllByText(/tag/).length).toBe(0)
expect(screen.queryAllByText(/other/).length).toBe(0)
})

it("hides the required actions cards", async () => {
history.push("?hidden_cards=action_required")
renderDashboard({ reportToRender: report })
expect(screen.getAllByText(/Subject title/).length).toBe(1)
expect(screen.queryAllByText(/Action required/).length).toBe(0)
expect(screen.getAllByText(/tag/).length).toBe(1)
expect(screen.getAllByText(/other/).length).toBe(1)
})

it("hides metrics not requiring action", async () => {
renderDashboard({ reportToRender: report })
fireEvent.click(screen.getByText(/Action required/))
expect(history.location.search).toEqual("?metrics_to_hide=no_action_required")
})

it("unhides metrics not requiring action", async () => {
history.push("?metrics_to_hide=no_action_required")
renderDashboard({ reportToRender: report })
fireEvent.click(screen.getByText(/Action required/))
expect(history.location.search).toEqual("")
})

it("hides the legend card", async () => {
history.push("?hidden_cards=legend")
renderDashboard({ reportToRender: report })
Expand Down
Loading

0 comments on commit b3c0b88

Please sign in to comment.