From de7c22aba83480de1b8036d476e5e2f3dc098e13 Mon Sep 17 00:00:00 2001 From: Frank Niessink Date: Mon, 17 Jun 2024 17:55:26 +0200 Subject: [PATCH] Dashboard card showing metrics requiring action. 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. --- .../dashboard/MetricsRequiringActionCard.css | 11 ++++ .../dashboard/MetricsRequiringActionCard.js | 63 +++++++++++++++++++ .../MetricsRequiringActionCard.test.js | 47 ++++++++++++++ .../src/header_footer/SettingsPanel.js | 1 + .../frontend/src/report/ReportDashboard.js | 12 ++++ components/frontend/src/sharedPropTypes.js | 2 +- components/frontend/src/utils.js | 5 +- components/frontend/src/utils.test.js | 1 + docs/src/changelog.md | 1 + 9 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 components/frontend/src/dashboard/MetricsRequiringActionCard.css create mode 100644 components/frontend/src/dashboard/MetricsRequiringActionCard.js create mode 100644 components/frontend/src/dashboard/MetricsRequiringActionCard.test.js diff --git a/components/frontend/src/dashboard/MetricsRequiringActionCard.css b/components/frontend/src/dashboard/MetricsRequiringActionCard.css new file mode 100644 index 0000000000..917061355a --- /dev/null +++ b/components/frontend/src/dashboard/MetricsRequiringActionCard.css @@ -0,0 +1,11 @@ +.ui.card.metrics_requiring_action .table { + margin-top: 0.3em; +} + +.ui.card.metrics_requiring_action .header { + margin-bottom: 0.3em; +} + +.ui.card.metrics_requiring_action .header.blue { + color: #2185d0; +} diff --git a/components/frontend/src/dashboard/MetricsRequiringActionCard.js b/components/frontend/src/dashboard/MetricsRequiringActionCard.js new file mode 100644 index 0000000000..4e3103ed21 --- /dev/null +++ b/components/frontend/src/dashboard/MetricsRequiringActionCard.js @@ -0,0 +1,63 @@ +import "./MetricsRequiringActionCard.css" + +import { bool, func } from "prop-types" + +import { Card, Header, Label, Table } from "../semantic_ui_react_wrappers" +import { reportPropType } from "../sharedPropTypes" +import { getMetricStatus, STATUS_COLORS, STATUS_NAME, STATUSES_REQUIRING_ACTION, sum } from "../utils" + +function metricStatuses(report) { + const statuses = {} + STATUSES_REQUIRING_ACTION.forEach((status) => {statuses[status] = 0}) + 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 +} + +export function MetricsRequiringActionCard({ onClick, report, selected }) { + const statuses = metricStatuses(report) + const tableRows = Object.keys(statuses).map((status) => ( + + {STATUS_NAME[status]} + + + + + )) + tableRows.push( + + Total + + + + + ) + const color = selected ? "blue" : null + return ( + + +
+ {"Action required"} +
+ + {tableRows} +
+
+
+ ) +} +MetricsRequiringActionCard.propTypes = { + onClick: func, + report: reportPropType, + selected: bool, +} diff --git a/components/frontend/src/dashboard/MetricsRequiringActionCard.test.js b/components/frontend/src/dashboard/MetricsRequiringActionCard.test.js new file mode 100644 index 0000000000..e082a8175b --- /dev/null +++ b/components/frontend/src/dashboard/MetricsRequiringActionCard.test.js @@ -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() +} + +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() +}) diff --git a/components/frontend/src/header_footer/SettingsPanel.js b/components/frontend/src/header_footer/SettingsPanel.js index 06a547f8f3..bb94ba4f02 100644 --- a/components/frontend/src/header_footer/SettingsPanel.js +++ b/components/frontend/src/header_footer/SettingsPanel.js @@ -51,6 +51,7 @@ export function SettingsPanel({ atReportsOverview, handleSort, settings, tags }) + diff --git a/components/frontend/src/report/ReportDashboard.js b/components/frontend/src/report/ReportDashboard.js index 0f9033de17..93312d3ac1 100644 --- a/components/frontend/src/report/ReportDashboard.js +++ b/components/frontend/src/report/ReportDashboard.js @@ -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 { @@ -86,6 +87,17 @@ 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_needed" + extraCards.push( + settings.metricsToHide.set(metric_requiring_action_selected ? "none" : "no_action_needed")} + selected={metric_requiring_action_selected} + /> + ) + } if (report.issue_tracker?.type && settings.hiddenCards.excludes("issues")) { const selected = settings.metricsToHide.value === "no_issues" extraCards.push( diff --git a/components/frontend/src/sharedPropTypes.js b/components/frontend/src/sharedPropTypes.js index e657f23199..f2f317a958 100644 --- a/components/frontend/src/sharedPropTypes.js +++ b/components/frontend/src/sharedPropTypes.js @@ -79,7 +79,7 @@ export const sortDirectionURLSearchQueryPropType = shape({ value: sortDirectionPropType, }) -export const hiddenCardsPropType = oneOf(["reports", "subjects", "tags", "issues", "legend"]) +export const hiddenCardsPropType = oneOf(["action_required", "reports", "subjects", "tags", "issues", "legend"]) export const metricsToHidePropType = oneOf(["all", "none", "no_action_needed", "no_issues"]) diff --git a/components/frontend/src/utils.js b/components/frontend/src/utils.js index 135b797ddb..61d96ee5b8 100644 --- a/components/frontend/src/utils.js +++ b/components/frontend/src/utils.js @@ -17,7 +17,8 @@ import { HyperLink } from "./widgets/HyperLink" export const MILLISECONDS_PER_HOUR = 60 * 60 * 1000 const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR -export const STATUSES = ["unknown", "target_not_met", "near_target_met", "target_met", "debt_target_met", "informative"] +export const STATUSES_REQUIRING_ACTION = ["unknown", "target_not_met", "near_target_met"] +export const STATUSES = STATUSES_REQUIRING_ACTION.concat("target_met", "debt_target_met", "informative") export const STATUS_COLORS = { informative: "blue", target_met: "green", @@ -344,7 +345,7 @@ getMetricIssueIds.propTypes = { } export function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1) + return string.charAt(0).toUpperCase() + string.slice(1).replaceAll("_", " ") } export function pluralize(word, count) { diff --git a/components/frontend/src/utils.test.js b/components/frontend/src/utils.test.js index 811402b3c4..a7511580a7 100644 --- a/components/frontend/src/utils.test.js +++ b/components/frontend/src/utils.test.js @@ -50,6 +50,7 @@ it("capitalizes strings", () => { expect(capitalize("ab")).toBe("Ab") expect(capitalize("aB")).toBe("AB") expect(capitalize("AB")).toBe("AB") + expect(capitalize("a_b")).toBe("A b") }) it("rounds numbers nicely", () => { diff --git a/docs/src/changelog.md b/docs/src/changelog.md index 2b2a8efcf9..f21e32829d 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -27,6 +27,7 @@ If your currently installed *Quality-time* version is not v5.13.0, please first - In the measurement entity status menu, the description of the menu items would say "undefined days" if the desired response time for the status had not been changed from its default value. Fixes [#8284](https://github.com/ICTU/quality-time/issues/8284). - Added a [versioning policy](versioning.md) to the documentation. Closes [#8748](https://github.com/ICTU/quality-time/issues/8748). - Allow for specifying supported source versions in the data model. Show the supported source version in the UI and the reference documentation. Closes [#8786](https://github.com/ICTU/quality-time/issues/8786). +- 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](https://github.com/ICTU/quality-time/issues/8938). ### Changed