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 de7c22a
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 3 deletions.
11 changes: 11 additions & 0 deletions components/frontend/src/dashboard/MetricsRequiringActionCard.css
Original file line number Diff line number Diff line change
@@ -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;
}
63 changes: 63 additions & 0 deletions components/frontend/src/dashboard/MetricsRequiringActionCard.js
Original file line number Diff line number Diff line change
@@ -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})

Check failure on line 11 in components/frontend/src/dashboard/MetricsRequiringActionCard.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Replace `statuses[status]·=·0` with `⏎········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) => (
<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]}>

Check failure on line 29 in components/frontend/src/dashboard/MetricsRequiringActionCard.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `·`
{statuses[status]}
</Label>
</Table.Cell>
</Table.Row>
))
tableRows.push(
<Table.Row key="total">
<Table.Cell><b>Total</b></Table.Cell>

Check failure on line 37 in components/frontend/src/dashboard/MetricsRequiringActionCard.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Replace `<b>Total</b>` with `⏎················<b>Total</b>⏎············`
<Table.Cell textAlign="right">
<Label size="small" color="black">
{sum(Object.values(statuses))}
</Label>
</Table.Cell>
</Table.Row>

Check failure on line 43 in components/frontend/src/dashboard/MetricsRequiringActionCard.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `,`
)
const color = selected ? "blue" : null
return (
<Card className="metrics_requiring_action" color={color} onClick={onClick} onKeyPress={onClick} tabIndex="0">
<Card.Content>
<Header as="h3" color={color} textAlign="center">
{"Action required"}
</Header>
<Table basic="very" compact="very" size="small">
<Table.Body>{tableRows}</Table.Body>
</Table>
</Card.Content>
</Card>
)
}
MetricsRequiringActionCard.propTypes = {
onClick: func,
report: reportPropType,
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"

Check failure on line 10 in components/frontend/src/dashboard/MetricsRequiringActionCard.test.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `,`
},
another_metric_uuid: {
status: "near_target_met"

Check failure on line 13 in components/frontend/src/dashboard/MetricsRequiringActionCard.test.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `,`
},
},
},
another_subject_uuid: {
metrics: {
yet_another_metric_uuid: {
status: "near_target_met"

Check failure on line 20 in components/frontend/src/dashboard/MetricsRequiringActionCard.test.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `,`
}

Check failure on line 21 in components/frontend/src/dashboard/MetricsRequiringActionCard.test.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `,`
}

Check failure on line 22 in components/frontend/src/dashboard/MetricsRequiringActionCard.test.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `,`
}

Check failure on line 23 in components/frontend/src/dashboard/MetricsRequiringActionCard.test.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Insert `,`
},
}

function renderMetricsRequiringActionCard({ selected = false } = {}) {
render(<MetricsRequiringActionCard report={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()
})
1 change: 1 addition & 0 deletions components/frontend/src/header_footer/SettingsPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ 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>
Expand Down
12 changes: 12 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,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(
<MetricsRequiringActionCard
key="metrics_requiring_action"
report={report}
onClick={() => 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(
Expand Down
2 changes: 1 addition & 1 deletion components/frontend/src/sharedPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand Down
5 changes: 3 additions & 2 deletions components/frontend/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions components/frontend/src/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
1 change: 1 addition & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit de7c22a

Please sign in to comment.