diff --git a/components/frontend/src/App.js b/components/frontend/src/App.js index 6ee06731d5..3f2aa1c60b 100644 --- a/components/frontend/src/App.js +++ b/components/frontend/src/App.js @@ -1,5 +1,6 @@ import "./App.css" +import { grey, orange } from "@mui/material/colors" import { createTheme, ThemeProvider } from "@mui/material/styles" import { Action } from "history" import history from "history/browser" @@ -14,11 +15,34 @@ import { registeredURLSearchParams } from "./hooks/url_search_query" import { isValidDate_YYYYMMDD, toISODateStringInCurrentTZ } from "./utils" import { showConnectionMessage, showMessage } from "./widgets/toast" -const theme = createTheme({ +let theme = createTheme({ colorSchemes: { dark: true, // Add a dark theme (light theme is available by default) }, - components: { MuiTooltip: { defaultProps: { arrow: true }, styleOverrides: { tooltip: { fontSize: "1em" } } } }, + components: { + MuiTooltip: { + defaultProps: { arrow: true }, + styleOverrides: { tooltip: { fontSize: "1em" } }, + }, + }, +}) + +theme = createTheme(theme, { + palette: { + todo: theme.palette.augmentColor({ color: { main: grey[600] }, name: "todo" }), + doing: theme.palette.augmentColor({ color: { main: theme.palette.info.main }, name: "doing" }), + done: theme.palette.augmentColor({ color: { main: theme.palette.success.main }, name: "done" }), + target_not_met: theme.palette.augmentColor({ + color: { main: theme.palette.error.main }, + name: "target_not_met", + }), + target_met: theme.palette.augmentColor({ color: { main: theme.palette.success.main }, name: "target_met" }), + near_target_met: theme.palette.augmentColor({ color: { main: orange[300] }, name: "near_target_met" }), + debt_target_met: theme.palette.augmentColor({ color: { main: grey[500] }, name: "debt_target_met" }), + informative: theme.palette.augmentColor({ color: { main: theme.palette.info.main }, name: "informative" }), + unknown: theme.palette.augmentColor({ color: { main: grey[300] }, name: "unknown" }), + total: theme.palette.augmentColor({ color: { main: grey[800] }, name: "total" }), + }, }) class App extends Component { diff --git a/components/frontend/src/PageContent.js b/components/frontend/src/PageContent.js index 87c998c4ef..0c66a0630a 100644 --- a/components/frontend/src/PageContent.js +++ b/components/frontend/src/PageContent.js @@ -1,11 +1,12 @@ +import Box from "@mui/material/Box" +import CircularProgress from "@mui/material/CircularProgress" import { bool, func, number, string } from "prop-types" import { useEffect, useState } from "react" -import { Container, Loader } from "semantic-ui-react" +import { Container } from "semantic-ui-react" import { get_measurements } from "./api/measurement" import { Report } from "./report/Report" import { ReportsOverview } from "./report/ReportsOverview" -import { Segment } from "./semantic_ui_react_wrappers" import { datePropType, optionalDatePropType, @@ -72,9 +73,17 @@ export function PageContent({ let content if (loading) { content = ( - - - + + + ) } else { const commonProps = { diff --git a/components/frontend/src/PageContent.test.js b/components/frontend/src/PageContent.test.js index 7f1c28c37d..1b7007f209 100644 --- a/components/frontend/src/PageContent.test.js +++ b/components/frontend/src/PageContent.test.js @@ -63,7 +63,7 @@ it("shows that the report was missing", async () => { it("shows the loading spinner", async () => { await renderPageContent({ loading: true }) - expect(screen.getAllByLabelText(/Loading/).length).toBe(1) + expect(screen.getAllByRole("progressbar").length).toBe(1) }) function expectMeasurementsCall(date, offset = 0) { diff --git a/components/frontend/src/dashboard/ExportCard.css b/components/frontend/src/dashboard/ExportCard.css deleted file mode 100644 index 7aeabb7464..0000000000 --- a/components/frontend/src/dashboard/ExportCard.css +++ /dev/null @@ -1,35 +0,0 @@ -.ui.card.export-data-card { - display: none; -} - -@media print { - .reportHeader { - display: flex; - align-items: center; - justify-content: space-between; - margin-right: -13px; - } - - .ui.card.export-data-card { - display: block; - min-width: 270px; - flex-shrink: 0; - } - - .ui.card.export-data-card.list .item { - display: flex; - overflow: hidden; - white-space: normal; - padding: 2px; - line-height: 1.4em; - } - - .ui.card.export-data-card .header { - overflow: hidden; - white-space: nowrap; - } - - .ui.card.export-data-card .list { - margin-top: 0.5em; - } -} diff --git a/components/frontend/src/dashboard/ExportCard.js b/components/frontend/src/dashboard/ExportCard.js deleted file mode 100644 index 3ff8f30416..0000000000 --- a/components/frontend/src/dashboard/ExportCard.js +++ /dev/null @@ -1,80 +0,0 @@ -import "./ExportCard.css" - -import { bool, string } from "prop-types" -import { Card, List } from "semantic-ui-react" - -import { childrenPropType, datePropType, reportPropType } from "../sharedPropTypes" - -function ExportCardItem({ children, url }) { - const item = children - return url ? ( - - {item} - - ) : ( - {item} - ) -} -ExportCardItem.propTypes = { - children: childrenPropType, - url: string, -} - -export function ExportCard({ lastUpdate, report, reportDate, isOverview = false }) { - const reportURL = new URLSearchParams(window.location.search).get("report_url") ?? window.location.href - const title = isOverview ? "About these reports" : "About this report" - const listItems = [ - - - {report.title} - - , - - - {"Report date: " + formatDate(reportDate ?? new Date())} - - , - - - - {"Generated: " + formatDate(lastUpdate) + ", " + formatTime(lastUpdate)} - - - , - - - - Quality-time v{process.env.REACT_APP_VERSION} - - - , - ] - return ( - - - - {title} - - {listItems} - - - ) -} -ExportCard.propTypes = { - isOverview: bool, - lastUpdate: datePropType, - report: reportPropType, - reportDate: datePropType, -} - -// Hard code en-GB to get European style dates and times. See https://github.com/ICTU/quality-time/issues/8381. - -function formatDate(date) { - return date.toLocaleDateString("en-GB", { year: "numeric", month: "2-digit", day: "2-digit" }).replace(/\//g, "-") -} - -function formatTime(date) { - return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }) -} diff --git a/components/frontend/src/dashboard/FilterCardWithTable.js b/components/frontend/src/dashboard/FilterCardWithTable.js index 12bde1f378..c294f5cb5b 100644 --- a/components/frontend/src/dashboard/FilterCardWithTable.js +++ b/components/frontend/src/dashboard/FilterCardWithTable.js @@ -1,14 +1,14 @@ +import { Table, TableBody } from "@mui/material" import { bool, func, string } from "prop-types" -import { Table } from "../semantic_ui_react_wrappers" import { childrenPropType } from "../sharedPropTypes" import { DashboardCard } from "./DashboardCard" export function FilterCardWithTable({ children, onClick, selected, title }) { return ( - - {children} +
+ {children}
) diff --git a/components/frontend/src/dashboard/IssuesCard.js b/components/frontend/src/dashboard/IssuesCard.js index 20576ce725..e8124c9045 100644 --- a/components/frontend/src/dashboard/IssuesCard.js +++ b/components/frontend/src/dashboard/IssuesCard.js @@ -1,9 +1,8 @@ -import { Chip } from "@mui/material" +import { Chip, TableCell, TableRow } from "@mui/material" import { bool, func } from "prop-types" -import { Table } from "../semantic_ui_react_wrappers" import { reportPropType } from "../sharedPropTypes" -import { capitalize, ISSUE_STATUS_THEME_COLORS } from "../utils" +import { capitalize } from "../utils" import { FilterCardWithTable } from "./FilterCardWithTable" function issueStatuses(report) { @@ -33,18 +32,12 @@ issueStatuses.propTypes = { function tableRows(report) { const statuses = issueStatuses(report) return Object.keys(statuses).map((status) => ( - - {capitalize(status)} - - - - + + {capitalize(status)} + + + + )) } tableRows.propTypes = { diff --git a/components/frontend/src/dashboard/LegendCard.js b/components/frontend/src/dashboard/LegendCard.js index 3b57f71028..7d1a11fa28 100644 --- a/components/frontend/src/dashboard/LegendCard.js +++ b/components/frontend/src/dashboard/LegendCard.js @@ -9,7 +9,7 @@ export function LegendCard() {   - + )) diff --git a/components/frontend/src/dashboard/MetricsRequiringActionCard.js b/components/frontend/src/dashboard/MetricsRequiringActionCard.js index b51abde1cf..ecaaf6a0c3 100644 --- a/components/frontend/src/dashboard/MetricsRequiringActionCard.js +++ b/components/frontend/src/dashboard/MetricsRequiringActionCard.js @@ -1,7 +1,7 @@ +import { Chip, TableCell, TableRow } from "@mui/material" 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 { STATUS_NAME, STATUSES_REQUIRING_ACTION } from "../metric/status" import { reportsPropType } from "../sharedPropTypes" import { getMetricStatus, sum } from "../utils" import { FilterCardWithTable } from "./FilterCardWithTable" @@ -30,26 +30,22 @@ metricStatuses.propTypes = { function tableRows(reports) { const statuses = metricStatuses(reports) const rows = Object.keys(statuses).map((status) => ( - - {STATUS_NAME[status]} - - - - + + {STATUS_NAME[status]} + + + + )) rows.push( - - + + Total - - - - - , + + + + + , ) return rows } diff --git a/components/frontend/src/dashboard/PageHeader.js b/components/frontend/src/dashboard/PageHeader.js new file mode 100644 index 0000000000..7b0fae2992 --- /dev/null +++ b/components/frontend/src/dashboard/PageHeader.js @@ -0,0 +1,43 @@ +import { Stack, Typography } from "@mui/material" + +import { datePropType, reportPropType } from "../sharedPropTypes" +import { HyperLink } from "../widgets/HyperLink" + +export function PageHeader({ lastUpdate, report, reportDate }) { + const reportURL = new URLSearchParams(window.location.search).get("report_url") ?? window.location.href + const title = report?.title ?? "Reports overview" + const changelogURL = `https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/changelog.html` + return ( + + + {title} + + {"Report date: " + formatDate(reportDate ?? new Date())} + + {"Generated: " + formatDate(lastUpdate) + ", " + formatTime(lastUpdate)} + + + Quality-time v{process.env.REACT_APP_VERSION} + + + ) +} +PageHeader.propTypes = { + lastUpdate: datePropType, + report: reportPropType, + reportDate: datePropType, +} + +// Hard code en-GB to get European style dates and times. See https://github.com/ICTU/quality-time/issues/8381. + +function formatDate(date) { + return date.toLocaleDateString("en-GB", { year: "numeric", month: "2-digit", day: "2-digit" }).replace(/\//g, "-") +} + +function formatTime(date) { + return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }) +} diff --git a/components/frontend/src/dashboard/ExportCard.test.js b/components/frontend/src/dashboard/PageHeader.test.js similarity index 65% rename from components/frontend/src/dashboard/ExportCard.test.js rename to components/frontend/src/dashboard/PageHeader.test.js index 69c1e09bdf..f3e019e44d 100644 --- a/components/frontend/src/dashboard/ExportCard.test.js +++ b/components/frontend/src/dashboard/PageHeader.test.js @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react" -import { ExportCard } from "./ExportCard" import { mockGetAnimations } from "./MockAnimations" +import { PageHeader } from "./PageHeader" beforeEach(() => mockGetAnimations()) @@ -15,6 +15,7 @@ const mockDateOfToday = new Date() const report = { report_uuid: "report_uuid", + title: "Title", subjects: { subject_uuid: { type: "subject_type", @@ -32,37 +33,38 @@ const report = { }, } -function renderExportCard({ isOverview = false, lastUpdate = new Date(), report = null, reportDate = null } = {}) { - render() +function renderPageHeader({ lastUpdate = new Date(), report = null, reportDate = null } = {}) { + render() } -it("displays correct title for an overview report", () => { - renderExportCard({ isOverview: true, report: report }) - expect(screen.getByText(/About these reports/)).toBeInTheDocument() +it("displays correct title for the reports overview", () => { + renderPageHeader({}) + expect(screen.getByText(/Reports overview/)).toBeInTheDocument() }) -it("displays correct title for a detailed report", () => { - renderExportCard({ report: report }) - expect(screen.getByText(/About this report/)).toBeInTheDocument() +it("displays correct title for a report", () => { + window.location.search = "?report_url=https://report/" + renderPageHeader({ report: report }) + expect(screen.getByText(/Title/)).toBeInTheDocument() }) it("displays dates in en-GB format", () => { - renderExportCard({ lastUpdate: mockLastUpdate, report: report, reportDate: mockReportDate }) + renderPageHeader({ lastUpdate: mockLastUpdate, report: report, reportDate: mockReportDate }) expect(screen.getByText(/Report date: 24-03-2024/)).toBeInTheDocument() expect(screen.getByText(/Generated: 26-03-2024, 12:34/)).toBeInTheDocument() }) it("displays report URL", () => { - renderExportCard({ report: report }) + renderPageHeader({ report: report }) expect(screen.getByTestId("reportUrl")).toBeInTheDocument() }) it("displays version link", () => { - renderExportCard({ lastUpdate: mockLastUpdate, report: report }) + renderPageHeader({ lastUpdate: mockLastUpdate, report: report }) expect(screen.getByTestId("version")).toBeInTheDocument() }) it("displays today as report date if no report date is provided", () => { - renderExportCard({ lastUpdate: mockLastUpdate, report: report }) + renderPageHeader({ lastUpdate: mockLastUpdate, report: report }) expect(screen.getByText(`Report date: ${mockDateOfToday}`)).toBeInTheDocument() }) diff --git a/components/frontend/src/errorMessage.js b/components/frontend/src/errorMessage.js index 39a8e16017..1f9e02e741 100644 --- a/components/frontend/src/errorMessage.js +++ b/components/frontend/src/errorMessage.js @@ -1,22 +1,19 @@ +import Grid from "@mui/material/Grid2" import { bool, object, oneOfType, string } from "prop-types" -import { Grid } from "semantic-ui-react" -import { Message } from "./semantic_ui_react_wrappers" +import { WarningMessage } from "./widgets/WarningMessage" export function ErrorMessage({ formatAsText, message, title }) { return ( - - - - {title} - {formatAsText ? ( - message - ) : ( -
{message}
- )} -
-
-
+ + + {formatAsText ? ( + message + ) : ( +
{message}
+ )} +
+
) } ErrorMessage.propTypes = { diff --git a/components/frontend/src/issue/IssueStatus.js b/components/frontend/src/issue/IssueStatus.js index a31ec09b40..fbdea36756 100644 --- a/components/frontend/src/issue/IssueStatus.js +++ b/components/frontend/src/issue/IssueStatus.js @@ -1,21 +1,36 @@ +import { Card, CardActionArea, CardContent, List, ListItem, Tooltip, Typography } from "@mui/material" import { bool, string } from "prop-types" import TimeAgo from "react-timeago" -import { Label, Popup } from "../semantic_ui_react_wrappers" import { issueStatusPropType, metricPropType, settingsPropType, stringsPropType } from "../sharedPropTypes" -import { getMetricIssueIds, ISSUE_STATUS_COLORS } from "../utils" -import { HyperLink } from "../widgets/HyperLink" +import { getMetricIssueIds } from "../utils" import { TimeAgoWithDate } from "../widgets/TimeAgoWithDate" function IssueWithoutTracker({ issueId }) { return ( - +

No issue tracker configured

+

+ Please configure an issue tracker by expanding the report title, selecting the ‘Issue + tracker’ tab, and configuring an issue tracker. +

+ } - header={"No issue tracker configured"} - trigger={} - /> + > + + + + + + {issueId} - ? + + + + + + ) } IssueWithoutTracker.propTypes = { @@ -35,41 +50,47 @@ IssuesWithoutTracker.propTypes = { issueIds: stringsPropType, } -function labelDetails(issueStatus, settings) { - let details = [{issueStatus.name || "?"}] +function cardDetails(issueStatus, settings) { + let details = [] if (issueStatus.summary && settings.showIssueSummary.value) { - details.push({issueStatus.summary}) + details.push({issueStatus.summary}) } if (issueStatus.created && settings.showIssueCreationDate.value) { details.push( - - Created - , + + + Created + + , ) } if (issueStatus.updated && settings.showIssueUpdateDate.value) { details.push( - - Updated - , + + + Updated + + , ) } if (issueStatus.duedate && settings.showIssueDueDate.value) { details.push( - - Due - , + + + Due + + , ) } if (issueStatus.release_name && settings.showIssueRelease.value) { - details.push(releaseLabel(issueStatus)) + details.push(release(issueStatus)) } if (issueStatus.sprint_name && settings.showIssueSprint.value) { - details.push(sprintLabel(issueStatus)) + details.push(sprint(issueStatus)) } - return details + return details.length > 0 ? {details} : null } -labelDetails.propTypes = { +cardDetails.propTypes = { issueStatus: issueStatusPropType, settings: settingsPropType, } @@ -81,31 +102,31 @@ releaseStatus.propTypes = { issueStatus: issueStatusPropType, } -function releaseLabel(issueStatus) { +function release(issueStatus) { const date = issueStatus.release_date ? : null return ( - + {prefixName(issueStatus.release_name, "Release")} {releaseStatus(issueStatus)} {date} - + ) } -releaseLabel.propTypes = { +release.propTypes = { issueStatus: issueStatusPropType, } -function sprintLabel(issueStatus) { +function sprint(issueStatus) { const sprintEnd = issueStatus.sprint_enddate ? ( <> ends ) : null return ( - + {prefixName(issueStatus.sprint_name, "Sprint")} ({issueStatus.sprint_state}) {sprintEnd} - + ) } -sprintLabel.propTypes = { +sprint.propTypes = { issueStatus: issueStatusPropType, } @@ -118,26 +139,24 @@ prefixName.propType = { prefix: string, } -function issueLabel(issueStatus, settings, error) { +function IssueCard({ issueStatus, settings, error }) { // The issue status can be unknown when the issue was added recently and the status hasn't been collected yet - const color = error ? "red" : ISSUE_STATUS_COLORS[issueStatus.status_category ?? "unknown"] - const label = ( - + const color = error ? "error" : (issueStatus.status_category ?? "unknown") + const onClick = issueStatus.landing_url ? () => window.open(issueStatus.landing_url) : null + return ( + + + + + {issueStatus.issue_id} - {issueStatus.name || "?"} + + {cardDetails(issueStatus, settings)} + + + ) - if (issueStatus.landing_url) { - // Without the span, the popup doesn't work - return ( - - {label} - - ) - } - return label } -issueLabel.propTypes = { +IssueCard.propTypes = { issueStatus: issueStatusPropType, settings: settingsPropType, error: string, @@ -154,15 +173,26 @@ function IssueWithTracker({ issueStatus, settings }) { popupHeader = "Parse error" popupContent = "Quality-time could not parse the data received from the issue tracker." } - let label = issueLabel(issueStatus, settings, popupHeader) + let card = if (!popupContent && issueStatus.created) { popupHeader = issueStatus.summary popupContent = issuePopupContent(issueStatus) } if (popupContent) { - label = + card = ( + +

{popupHeader}

+

{popupContent}

+ + } + > + {card} +
+ ) } - return label + return card } IssueWithTracker.propTypes = { issueStatus: issueStatusPropType, diff --git a/components/frontend/src/issue/IssueStatus.test.js b/components/frontend/src/issue/IssueStatus.test.js index 4bfdca6da4..fd8515d1bf 100644 --- a/components/frontend/src/issue/IssueStatus.test.js +++ b/components/frontend/src/issue/IssueStatus.test.js @@ -1,9 +1,8 @@ -import { render, screen, waitFor } from "@testing-library/react" +import { fireEvent, render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import history from "history/browser" import { createTestableSettings } from "../__fixtures__/fixtures" -import { ISSUE_STATUS_COLORS } from "../utils" import { IssueStatus } from "./IssueStatus" function renderIssueStatus({ @@ -62,47 +61,22 @@ beforeEach(() => { }) it("displays the issue id", () => { - const { queryByText } = renderIssueStatus() - expect(queryByText(/123/)).not.toBe(null) -}) - -it("displays the status", () => { - const { queryByText } = renderIssueStatus() - expect(queryByText(/in progress/)).not.toBe(null) -}) - -it("displays the status category doing", () => { - renderIssueStatus({ statusCategory: "doing" }) - expect(screen.getByText(/123/).className).toContain("blue") -}) - -it("displays the status category todo", () => { - renderIssueStatus({ statusCategory: "todo" }) - expect(screen.getByText(/123/).className).toContain("grey") -}) - -it("displays the status category done", () => { - renderIssueStatus({ statusCategory: "done" }) - expect(screen.getByText(/123/).className).toContain("green") -}) - -it("displays a missing status category as unknown", () => { renderIssueStatus() - Object.values(ISSUE_STATUS_COLORS) - .filter((color) => color !== null) - .forEach((color) => { - expect(screen.getByText(/123/).className).not.toContain(color) - }) + expect(screen.queryByText(/123/)).not.toBe(null) }) -it("displays the issue landing url", async () => { +it("opens the issue landing url", async () => { + window.open = jest.fn() const { queryByText } = renderIssueStatus() - expect(queryByText(/123/).closest("a").href).toBe("https://issue/") + fireEvent.click(queryByText(/123/)) + expect(window.open).toHaveBeenCalledWith("https://issue") }) -it("does not display an url if the issue has no landing url", async () => { - const { queryByText } = renderIssueStatus({ landingUrl: null }) - expect(queryByText(/123/).closest("a")).toBe(null) +it("does not open an url if the issue has no landing url", async () => { + window.open = jest.fn() + const { queryByText } = renderIssueStatus({ landingUrl: "" }) + fireEvent.click(queryByText(/123/)) + expect(window.open).not.toHaveBeenCalled() }) it("displays a question mark as status if the issue has no status", () => { diff --git a/components/frontend/src/issue/IssuesRows.js b/components/frontend/src/issue/IssuesRows.js index 6f2960689d..f740d8a6df 100644 --- a/components/frontend/src/issue/IssuesRows.js +++ b/components/frontend/src/issue/IssuesRows.js @@ -1,6 +1,6 @@ +import Grid from "@mui/material/Grid2" import { bool, func, node, string } from "prop-types" import { useState } from "react" -import { Grid } from "semantic-ui-react" import { add_metric_issue, set_metric_attribute } from "../api/metric" import { get_report_issue_tracker_suggestions } from "../api/report" @@ -116,64 +116,56 @@ export function IssuesRows({ metric, metric_uuid, reload, report, target }) { } return ( <> - - + + + + } + editableComponent={ + <> + + + + - - } - editableComponent={ - <> - - - - - - - - } - /> - + + + } + /> {getMetricIssueIds(metric).length > 0 && !issueTrackerConfigured && ( - - - - - + + + )} {(metric.issue_status ?? []) .filter((issue_status) => issue_status.connection_error) .map((issue_status) => ( - - - - - + + + ))} {(metric.issue_status ?? []) .filter((issue_status) => issue_status.parse_error) .map((issue_status) => ( - - - - - + + + ))} ) diff --git a/components/frontend/src/measurement/MeasurementSources.js b/components/frontend/src/measurement/MeasurementSources.js index 049ff10b0c..692554ff76 100644 --- a/components/frontend/src/measurement/MeasurementSources.js +++ b/components/frontend/src/measurement/MeasurementSources.js @@ -2,8 +2,7 @@ import { SourceStatus } from "./SourceStatus" export function MeasurementSources({ metric }) { const sources = metric.latest_measurement?.sources ?? [] - return sources.map((source, index) => [ - index > 0 && ", ", + return sources.map((source) => [ , ]) } diff --git a/components/frontend/src/measurement/MeasurementSources.test.js b/components/frontend/src/measurement/MeasurementSources.test.js index 8cebc97468..18b05dabf6 100644 --- a/components/frontend/src/measurement/MeasurementSources.test.js +++ b/components/frontend/src/measurement/MeasurementSources.test.js @@ -37,5 +37,5 @@ it("renders multiple measurement sources", () => { /> , ) - expect(screen.getAllByText(/Source name 1, Source name 2/).length).toBe(1) + expect(screen.getAllByText(/Source name 1.*Source name 2/).length).toBe(1) }) diff --git a/components/frontend/src/measurement/MeasurementTarget.js b/components/frontend/src/measurement/MeasurementTarget.js index 134e21d7dc..17bd0629e2 100644 --- a/components/frontend/src/measurement/MeasurementTarget.js +++ b/components/frontend/src/measurement/MeasurementTarget.js @@ -1,7 +1,7 @@ +import { Tooltip } from "@mui/material" import { useContext } from "react" import { DataModel } from "../context/DataModel" -import { Label, Popup } from "../semantic_ui_react_wrappers" import { metricPropType } from "../sharedPropTypes" import { formatMetricDirection, @@ -12,6 +12,7 @@ import { getMetricTarget, isValidDate_YYYYMMDD, } from "../utils" +import { Label } from "../widgets/Label" function popupText(metric, debtEndDateInThePast, allIssuesDone, dataModel) { const unit = formatMetricScaleAndUnit(metric, dataModel) @@ -59,11 +60,11 @@ export function MeasurementTarget({ metric }) { const today = new Date() debtEndDateInThePast = endDate.toISOString().split("T")[0] < today.toISOString().split("T")[0] } - const label = allIssuesDone || debtEndDateInThePast ? : {target} + const label = allIssuesDone || debtEndDateInThePast ? : target return ( - - {popupText(metric, debtEndDateInThePast, allIssuesDone, dataModel)} - + {popupText(metric, debtEndDateInThePast, allIssuesDone, dataModel)}}> + {label} + ) } MeasurementTarget.propTypes = { diff --git a/components/frontend/src/measurement/MeasurementValue.js b/components/frontend/src/measurement/MeasurementValue.js index f9d7e58c63..0566fdfb52 100644 --- a/components/frontend/src/measurement/MeasurementValue.js +++ b/components/frontend/src/measurement/MeasurementValue.js @@ -1,10 +1,10 @@ import "./MeasurementValue.css" +import { Alert, Tooltip, Typography } from "@mui/material" import { bool, string } from "prop-types" import { useContext } from "react" import { DataModel } from "../context/DataModel" -import { Label, Message, Popup } from "../semantic_ui_react_wrappers" import { datePropType, measurementPropType, metricPropType } from "../sharedPropTypes" import { IGNORABLE_SOURCE_ENTITY_STATUSES, SOURCE_ENTITY_STATUS_NAME } from "../source/source_entity_status" import { @@ -18,6 +18,7 @@ import { sum, } from "../utils" import { IgnoreIcon, LoadingIcon } from "../widgets/icons" +import { Label } from "../widgets/Label" import { TimeAgoWithDate } from "../widgets/TimeAgoWithDate" import { WarningMessage } from "../widgets/WarningMessage" @@ -30,14 +31,20 @@ function measurementValueLabel(hasIgnoredEntities, stale, updating, value) { value ) if (stale) { - return + return ( + + + + ) } if (updating) { return ( - + + + ) } return {measurementValue} @@ -101,49 +108,43 @@ export function MeasurementValue({ metric, reportDate }) { const requested = isMeasurementRequested(metric) const hasIgnoredEntities = sum(ignoredEntitiesCount(metric.latest_measurement)) > 0 return ( - + + This may indicate a problem with Quality-time itself. Please contact a system administrator. + + + The source configuration of this metric was changed after the latest measurement. + + + An update of the latest measurement was requested by a user. + + {hasIgnoredEntities && ( + + + {`Ignored ${unit}`} + + {ignoredEntitiesMessage(metric.latest_measurement, unit)} + + )} + {metric.latest_measurement && ( + <> + + {metric.status ? "The metric was last measured" : "Last measurement attempt"} + +
+ + {metric.status ? "The current value was first measured" : "The value is unknown since"} + + + )} + + } > - - - - {hasIgnoredEntities && ( - - {`Ignored ${unit}`} - - } - content={ignoredEntitiesMessage(metric.latest_measurement, unit)} - /> - )} - {metric.latest_measurement && ( - <> - - {metric.status ? "The metric was last measured" : "Last measurement attempt"} - -
- - {metric.status ? "The current value was first measured" : "The value is unknown since"} - - - )} -
+ {measurementValueLabel(hasIgnoredEntities, stale, outdated || requested, value)} + ) } MeasurementValue.propTypes = { diff --git a/components/frontend/src/measurement/MeasurementValue.test.js b/components/frontend/src/measurement/MeasurementValue.test.js index 1af6c4721e..62f5e67c9d 100644 --- a/components/frontend/src/measurement/MeasurementValue.test.js +++ b/components/frontend/src/measurement/MeasurementValue.test.js @@ -55,7 +55,6 @@ it("renders an outdated value", async () => { }, }) const measurementValue = screen.getByText(/1/) - expect(measurementValue.className).toContain("yellow") expect(screen.getAllByTestId("LoopIcon").length).toBe(1) await userEvent.hover(measurementValue) await waitFor(() => { @@ -73,7 +72,6 @@ it("renders a value for which a measurement was requested", async () => { measurement_requested: now, }) const measurementValue = screen.getByText(/1/) - expect(measurementValue.className).toContain("yellow") expect(screen.getAllByTestId("LoopIcon").length).toBe(1) await userEvent.hover(measurementValue) await waitFor(() => { @@ -89,7 +87,6 @@ it("renders a value for which a measurement was requested, but which is now up t measurement_requested: "2024-01-01T00:00:00", }) const measurementValue = screen.getByText(/1/) - expect(measurementValue.className).not.toContain("yellow") expect(screen.queryAllByTestId("LoopIcon").length).toBe(0) await userEvent.hover(measurementValue) await waitFor(() => { diff --git a/components/frontend/src/measurement/Overrun.js b/components/frontend/src/measurement/Overrun.js index 4f29c6016d..a7d46fe9d7 100644 --- a/components/frontend/src/measurement/Overrun.js +++ b/components/frontend/src/measurement/Overrun.js @@ -1,8 +1,18 @@ +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, +} from "@mui/material" import { string } from "prop-types" import { useContext } from "react" import { DataModel } from "../context/DataModel" -import { Header, Popup, Table } from "../semantic_ui_react_wrappers" import { datesPropType, measurementsPropType, metricPropType, reportPropType } from "../sharedPropTypes" import { getMetricResponseOverrun, pluralize } from "../utils" import { StatusIcon } from "./StatusIcon" @@ -23,59 +33,60 @@ export function Overrun({ metric_uuid, metric, report, measurements, dates }) { const period = `${sortedDates.at(0).toLocaleDateString()} - ${sortedDates.at(-1).toLocaleDateString()}` const content = ( <> -
- - Metric reaction time overruns - In the period {period} - -
- - - - - When did the metric need action? - - - How long did it take to react? - - - - Status - Start - End - Actual - Desired - Overrun - - - - {overruns.map((overrun) => ( - - - - - {overrun.start.split("T")[0]} - {overrun.end.split("T")[0]} - {formatDays(overrun.actual_response_time)} - {formatDays(overrun.desired_response_time)} - {formatDays(overrun.overrun)} - - ))} - - - - - Total - - - {triggerText} - - - -
+ Metric reaction time overruns in the period {period} + + + + + + When did the metric need action? + + + How long did it take to react? + + + + Status + Start + End + Actual + Desired + Overrun + + + + {overruns.map((overrun) => ( + + + + + {overrun.start.split("T")[0]} + {overrun.end.split("T")[0]} + {formatDays(overrun.actual_response_time)} + {formatDays(overrun.desired_response_time)} + {formatDays(overrun.overrun)} + + ))} + + + + + Total + + + {triggerText} + + + +
+
) - return + return ( + + {trigger} + + ) } Overrun.propTypes = { dates: datesPropType, diff --git a/components/frontend/src/measurement/SourceStatus.js b/components/frontend/src/measurement/SourceStatus.js index 48ac30f5a4..e3d1d0f756 100644 --- a/components/frontend/src/measurement/SourceStatus.js +++ b/components/frontend/src/measurement/SourceStatus.js @@ -1,10 +1,11 @@ +import { Tooltip, Typography } from "@mui/material" import { useContext } from "react" import { DataModel } from "../context/DataModel" -import { Label, Popup } from "../semantic_ui_react_wrappers" import { measurementSourcePropType, metricPropType } from "../sharedPropTypes" import { getMetricName, getSourceName } from "../utils" import { HyperLink } from "../widgets/HyperLink" +import { Label } from "../widgets/Label" export function SourceStatus({ metric, measurement_source }) { const dataModel = useContext(DataModel) @@ -17,7 +18,9 @@ export function SourceStatus({ metric, measurement_source }) { const configError = !dataModel.metrics[metric.type].sources.includes(source.type) function source_label() { return measurement_source.landing_url ? ( - {source_name} + + {source_name} + ) : ( source_name ) @@ -36,13 +39,18 @@ export function SourceStatus({ metric, measurement_source }) { header = "Parse error" } return ( - {source_label()}} - /> + +

{header}

+ {content} + + } + > + + + +
) } else { return source_label() diff --git a/components/frontend/src/measurement/StatusIcon.js b/components/frontend/src/measurement/StatusIcon.js index fd73ed86b3..5a44aa0003 100644 --- a/components/frontend/src/measurement/StatusIcon.js +++ b/components/frontend/src/measurement/StatusIcon.js @@ -1,7 +1,7 @@ import { Avatar, Tooltip } from "@mui/material" import { instanceOf, oneOfType, string } from "prop-types" -import { STATUS_COLORS_MUI, STATUS_ICONS, STATUS_SHORT_NAME, statusPropType } from "../metric/status" +import { STATUS_ICONS, STATUS_SHORT_NAME, statusPropType } from "../metric/status" import { TimeAgoWithDate } from "../widgets/TimeAgoWithDate" export function StatusIcon({ status, statusStart, size }) { @@ -9,7 +9,7 @@ export function StatusIcon({ status, statusStart, size }) { const sizes = { small: 20, undefined: 32 } const statusName = STATUS_SHORT_NAME[status] // Use Avatar to create a round inverted icon: - const iconStyle = { width: sizes[size], height: sizes[size], bgcolor: STATUS_COLORS_MUI[status] } + const iconStyle = { width: sizes[size], height: sizes[size], bgcolor: `${status}.main` } const icon = ( {STATUS_ICONS[status]} diff --git a/components/frontend/src/measurement/TimeLeft.js b/components/frontend/src/measurement/TimeLeft.js index d89e8ae5d4..31e5c448f3 100644 --- a/components/frontend/src/measurement/TimeLeft.js +++ b/components/frontend/src/measurement/TimeLeft.js @@ -1,6 +1,8 @@ -import { Label, Popup } from "../semantic_ui_react_wrappers" +import { Tooltip } from "@mui/material" + import { metricPropType, reportPropType } from "../sharedPropTypes" import { days, getMetricResponseDeadline, getMetricResponseTimeLeft, pluralize } from "../utils" +import { Label } from "../widgets/Label" import { TimeAgoWithDate } from "../widgets/TimeAgoWithDate" export function TimeLeft({ metric, report }) { @@ -12,15 +14,15 @@ export function TimeLeft({ metric, report }) { const daysLeft = days(Math.max(0, timeLeft)) const triggerText = `${daysLeft} ${pluralize("day", daysLeft)}` let deadlineLabel = "Deadline to address this metric was" - let trigger = + let trigger = if (timeLeft >= 0) { deadlineLabel = "Time left to address this metric is" - trigger = {triggerText} + trigger = triggerText } return ( - - {deadlineLabel}. - + {deadlineLabel}}> + {trigger} + ) } TimeLeft.propTypes = { diff --git a/components/frontend/src/metric/MetricConfigurationParameters.js b/components/frontend/src/metric/MetricConfigurationParameters.js index abbdb6ac36..884cc8c861 100644 --- a/components/frontend/src/metric/MetricConfigurationParameters.js +++ b/components/frontend/src/metric/MetricConfigurationParameters.js @@ -1,6 +1,7 @@ +import Grid from "@mui/material/Grid2" import { func, string } from "prop-types" import { useContext } from "react" -import { Grid, Header } from "semantic-ui-react" +import { Header } from "semantic-ui-react" import { set_metric_attribute } from "../api/metric" import { DataModel } from "../context/DataModel" @@ -191,61 +192,51 @@ export function MetricConfigurationParameters({ metric, metric_uuid, reload, rep const dataModel = useContext(DataModel) const metricScale = getMetricScale(metric, dataModel) return ( - - - - - - - - - - - - - - - - - - - - {metricScale !== "version_number" && ( - - - - )} - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + {metricScale !== "version_number" && } + + + + + + + + + + ) } diff --git a/components/frontend/src/metric/MetricDebtParameters.js b/components/frontend/src/metric/MetricDebtParameters.js index a473eaabc7..84fa4cf92d 100644 --- a/components/frontend/src/metric/MetricDebtParameters.js +++ b/components/frontend/src/metric/MetricDebtParameters.js @@ -1,5 +1,5 @@ +import Grid from "@mui/material/Grid2" import { func, string } from "prop-types" -import { Grid } from "semantic-ui-react" import { set_metric_attribute, set_metric_debt } from "../api/metric" import { EDIT_REPORT_PERMISSION } from "../context/Permissions" @@ -94,35 +94,30 @@ TechnicalDebtEndDate.propTypes = { export function MetricDebtParameters({ metric, metric_uuid, reload, report }) { return ( - - - - - - - - - - - - + + + + + + + + + + - - - set_metric_attribute(metric_uuid, "comment", value, reload)} - value={metric.comment} - /> - - + + set_metric_attribute(metric_uuid, "comment", value, reload)} + value={metric.comment} + /> + ) } diff --git a/components/frontend/src/metric/Target.js b/components/frontend/src/metric/Target.js index fdaf4b49a7..d17b301e7f 100644 --- a/components/frontend/src/metric/Target.js +++ b/components/frontend/src/metric/Target.js @@ -1,5 +1,5 @@ import HelpIcon from "@mui/icons-material/Help" -import { Box, Stack, Typography } from "@mui/material" +import { Box, Stack, Tooltip, Typography } from "@mui/material" import { bool, func, oneOf, string } from "prop-types" import { useContext } from "react" @@ -9,7 +9,6 @@ import { EDIT_REPORT_PERMISSION } from "../context/Permissions" import { IntegerInput } from "../fields/IntegerInput" import { StringInput } from "../fields/StringInput" import { StatusIcon } from "../measurement/StatusIcon" -import { Popup } from "../semantic_ui_react_wrappers" import { childrenPropType, labelPropType, metricPropType, scalePropType } from "../sharedPropTypes" import { capitalize, @@ -18,7 +17,7 @@ import { formatMetricValue, getMetricScale, } from "../utils" -import { STATUS_COLORS_MUI, STATUS_SHORT_NAME, statusPropType } from "./status" +import { STATUS_SHORT_NAME, statusPropType } from "./status" function smallerThan(target1, target2) { const t1 = target1 ?? `${Number.POSITIVE_INFINITY}` @@ -52,7 +51,7 @@ function ColoredSegment({ children, color, show, status }) { return null } return ( - + {STATUS_SHORT_NAME[status]}  @@ -232,7 +231,7 @@ TargetVisualiser.propTypes = { metric: metricPropType, } -function TargetLabel({ label, metric, position, targetType }) { +function TargetLabel({ label, metric, targetType }) { const dataModel = useContext(DataModel) const metricType = dataModel.metrics[metric.type] const defaultTarget = metricType[targetType] @@ -245,32 +244,33 @@ function TargetLabel({ label, metric, position, targetType }) { return ( ) } TargetLabel.propTypes = { label: labelPropType, metric: metricPropType, - position: string, targetType: string, } -export function Target({ label, labelPosition, metric, metric_uuid, reload, target_type }) { +export function Target({ label, metric, metric_uuid, reload, target_type }) { const dataModel = useContext(DataModel) const metricScale = getMetricScale(metric, dataModel) const metricDirectionPrefix = formatMetricDirection(metric, dataModel) const targetValue = metric[target_type] const unit = formatMetricScaleAndUnit(metric, dataModel) - const targetLabel = + const targetLabel = if (metricScale === "version_number") { return ( { it("shows help", async () => { renderMetricTarget({ type: "violations", target: "10", near_target: "15" }) - await userEvent.tab() + await userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expect(screen.queryAllByText(/How measurement values are evaluated/).length).toBe(1) }) @@ -100,7 +100,7 @@ function expectNotVisible(...matchers) { it("shows help for evaluated metric without tech debt", async () => { renderMetricTarget({ type: "violations", target: "10", near_target: "15" }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target met/, @@ -122,7 +122,7 @@ it("shows help for evaluated metric with tech debt", async () => { near_target: "20", accept_debt: true, }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target met/, @@ -139,7 +139,7 @@ it("shows help for evaluated metric with tech debt", async () => { it("shows help for evaluated metric with tech debt if debt target is missing", async () => { renderMetricTarget({ type: "violations", target: "10", near_target: "20", accept_debt: true }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target met/, @@ -162,7 +162,7 @@ it("shows help for evaluated metric with tech debt with end date", async () => { accept_debt: true, debt_end_date: "3000-01-01", }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target met/, @@ -186,7 +186,7 @@ it("shows help for evaluated metric with tech debt with end date in the past", a accept_debt: true, debt_end_date: "2000-01-01", }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target met/, @@ -208,7 +208,7 @@ it("shows help for evaluated metric with tech debt completely overlapping near t near_target: "20", accept_debt: true, }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target met/, @@ -224,7 +224,7 @@ it("shows help for evaluated metric with tech debt completely overlapping near t it("shows help for evaluated metric without tech debt and target completely overlapping near target", async () => { renderMetricTarget({ type: "violations", target: "10", near_target: "10" }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible(/Target met/, /≦ 10 violations/, /Target not met/, /> 10 violations/) expectNotVisible(/Debt target met/, /Near target met/) @@ -233,7 +233,7 @@ it("shows help for evaluated metric without tech debt and target completely over it("shows help for evaluated more-is-better metric without tech debt", async () => { renderMetricTarget({ type: "violations", target: "15", near_target: "10", direction: ">" }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target not met/, @@ -256,7 +256,7 @@ it("shows help for evaluated more-is-better metric with tech debt", async () => accept_debt: true, direction: ">", }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target not met/, @@ -279,7 +279,7 @@ it("shows help for evaluated more-is-better metric with tech debt and missing de accept_debt: true, direction: ">", }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target not met/, @@ -302,7 +302,7 @@ it("shows help for evaluated more-is-better metric with tech debt completely ove accept_debt: true, direction: ">", }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible( /Target not met/, @@ -318,7 +318,7 @@ it("shows help for evaluated more-is-better metric with tech debt completely ove it("shows help for evaluated more-is-better metric without tech debt and target completely overlapping near target", async () => { renderMetricTarget({ type: "violations", target: "15", near_target: "15", direction: ">" }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible(/Target not met/, /< 15 violations/, /Target met/, /≧ 15 violations/) expectNotVisible(/Near target met/, /Debt target met/) @@ -327,7 +327,7 @@ it("shows help for evaluated more-is-better metric without tech debt and target it("shows help for evaluated metric without tech debt and zero target completely overlapping near target", async () => { renderMetricTarget({ type: "violations", target: "0", near_target: "0", direction: ">" }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible(/Target met/, /≧ 0 violations/) expectNotVisible(/Debt target met/, /Near target met/, /Target not met/) @@ -336,7 +336,7 @@ it("shows help for evaluated metric without tech debt and zero target completely it("shows help for informative metric", async () => { renderMetricTarget({ type: "violations", evaluate_targets: false }) - await userEvent.tab() + userEvent.hover(screen.getByTestId("HelpIcon")) await waitFor(() => { expectVisible(/Informative/, /violations are not evaluated/) expectNotVisible(/Target met/, /Debt target met/, /Near target met/, /Target not met/) diff --git a/components/frontend/src/metric/TrendGraph.js b/components/frontend/src/metric/TrendGraph.js index ca509a5201..6b804d0966 100644 --- a/components/frontend/src/metric/TrendGraph.js +++ b/components/frontend/src/metric/TrendGraph.js @@ -1,5 +1,4 @@ import { useContext } from "react" -import { Message } from "semantic-ui-react" import { VictoryAxis, VictoryChart, VictoryLabel, VictoryLine, VictoryTheme } from "victory" import { DarkMode } from "../context/DarkMode" @@ -7,7 +6,7 @@ import { DataModel } from "../context/DataModel" import { loadingPropType, measurementsPropType, metricPropType } from "../sharedPropTypes" import { capitalize, formatMetricScaleAndUnit, getMetricName, getMetricScale, niceNumber, scaledNumber } from "../utils" import { LoadingPlaceHolder } from "../widgets/Placeholder" -import { FailedToLoadMeasurementsWarningMessage, WarningMessage } from "../widgets/WarningMessage" +import { FailedToLoadMeasurementsWarningMessage, InfoMessage, WarningMessage } from "../widgets/WarningMessage" function measurementAttributeAsNumber(metric, measurement, field, dataModel) { const scale = getMetricScale(metric, dataModel) @@ -22,10 +21,9 @@ export function TrendGraph({ metric, measurements, loading }) { const estimatedTotalChartHeight = chartHeight + 200 // Estimate of the height including title and axis if (getMetricScale(metric, dataModel) === "version_number") { return ( - + + Trend graphs are not supported for metrics with a version number scale. + ) } if (loading === "failed") { @@ -36,10 +34,9 @@ export function TrendGraph({ metric, measurements, loading }) { } if (measurements.length === 0) { return ( - + + A trend graph can not be displayed until this metric has measurements. + ) } const metricName = getMetricName(metric, dataModel) diff --git a/components/frontend/src/metric/status.js b/components/frontend/src/metric/status.js index 3ef465e9f7..f6b5990e8d 100644 --- a/components/frontend/src/metric/status.js +++ b/components/frontend/src/metric/status.js @@ -1,7 +1,6 @@ // Metric status constants import { Bolt, Check, Money, QuestionMark, Warning } from "@mui/icons-material" -import { blue, green, grey, orange, red } from "@mui/material/colors" import { oneOf } from "prop-types" import { HyperLink } from "../widgets/HyperLink" @@ -25,14 +24,6 @@ export const STATUS_COLORS_RGB = { informative: "rgb(0,165,255)", unknown: "rgb(245,245,245)", } -export const STATUS_COLORS_MUI = { - target_not_met: red[700], - target_met: green[600], - near_target_met: orange[300], - debt_target_met: grey[500], - informative: blue[500], - unknown: grey[300], -} export const STATUS_ICONS = { target_met: , near_target_met: , diff --git a/components/frontend/src/notification/NotificationDestinations.js b/components/frontend/src/notification/NotificationDestinations.js index 28ce09fc8e..cc257b7f66 100644 --- a/components/frontend/src/notification/NotificationDestinations.js +++ b/components/frontend/src/notification/NotificationDestinations.js @@ -1,6 +1,6 @@ import { Stack } from "@mui/material" +import Grid from "@mui/material/Grid2" import { func, objectOf, string } from "prop-types" -import { Grid } from "semantic-ui-react" import { add_notification_destination, @@ -9,13 +9,13 @@ import { } from "../api/notification" import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from "../context/Permissions" import { StringInput } from "../fields/StringInput" -import { Message } from "../semantic_ui_react_wrappers" import { destinationPropType } from "../sharedPropTypes" import { ButtonRow } from "../widgets/ButtonRow" import { AddButton } from "../widgets/buttons/AddButton" import { DeleteButton } from "../widgets/buttons/DeleteButton" import { HyperLink } from "../widgets/HyperLink" import { LabelWithHelp } from "../widgets/LabelWithHelp" +import { InfoMessage } from "../widgets/WarningMessage" function NotificationDestination({ destination, destination_uuid, reload, report_uuid }) { const help_url = @@ -23,47 +23,41 @@ function NotificationDestination({ destination, destination_uuid, reload, report const teams_hyperlink = Microsoft Teams return ( - - - - { - set_notification_destination_attributes( - report_uuid, - destination_uuid, - { name: value }, - reload, - ) - }} - value={destination.name} - /> - - - Paste a {teams_hyperlink} webhook URL here.} - hoverable - /> - } - placeholder="https://example.webhook.office.com/webhook..." - set_value={(value) => { - set_notification_destination_attributes( - report_uuid, - destination_uuid, - { webhook: value, url: window.location.href }, - reload, - ) - }} - value={destination.webhook} - /> - - + + + { + set_notification_destination_attributes( + report_uuid, + destination_uuid, + { name: value }, + reload, + ) + }} + value={destination.name} + /> + + + Paste a {teams_hyperlink} webhook URL here.} /> + } + placeholder="https://example.webhook.office.com/webhook..." + set_value={(value) => { + set_notification_destination_attributes( + report_uuid, + destination_uuid, + { webhook: value, url: window.location.href }, + reload, + ) + }} + value={destination.webhook} + /> + + {notification_destinations.length === 0 ? ( - - No notification destinations -

No notification destinations have been configured yet.

-
+ + No notification destinations have been configured yet. + ) : ( notification_destinations )} @@ -121,7 +114,7 @@ export function NotificationDestinations({ destinations, reload, report_uuid }) /> } /> - +
) } NotificationDestinations.propTypes = { diff --git a/components/frontend/src/report/IssueTracker.js b/components/frontend/src/report/IssueTracker.js index 6c8a8c9169..c91e17e9a4 100644 --- a/components/frontend/src/report/IssueTracker.js +++ b/components/frontend/src/report/IssueTracker.js @@ -1,6 +1,8 @@ +import { Stack } from "@mui/material" +import Grid from "@mui/material/Grid2" import { func } from "prop-types" import { useContext, useEffect, useState } from "react" -import { Grid, Header } from "semantic-ui-react" +import { Header } from "semantic-ui-react" import { get_report_issue_tracker_options, set_report_issue_tracker_attribute } from "../api/report" import { DataModel } from "../context/DataModel" @@ -104,111 +106,96 @@ export function IssueTracker({ report, reload }) { const epic_link = report.issue_tracker?.parameters?.epic_link return ( - - - - set_report_issue_tracker_attribute(report_uuid, "type", value, reload)} - value={report.issue_tracker?.type} - /> - - - set_report_issue_tracker_attribute(report_uuid, "url", value, reload)} - value={report.issue_tracker?.parameters?.url} - /> - - - - - - set_report_issue_tracker_attribute(report_uuid, "username", value, reload) - } - value={report.issue_tracker?.parameters?.username} - /> - - - - set_report_issue_tracker_attribute(report_uuid, "password", value, reload) - } - value={report.issue_tracker?.parameters?.password} - /> - - - - - - set_report_issue_tracker_attribute(report_uuid, "private_token", value, reload) - } - value={report.issue_tracker?.parameters?.private_token} - /> - - - - - - } - options={projectOptions} - placeholder="None" - set_value={(value) => - set_report_issue_tracker_attribute(report_uuid, "project_key", value, reload) - } - value={project_key} - /> - - - - } - options={issueTypeOptions} - placeholder="None" - set_value={(value) => - set_report_issue_tracker_attribute(report_uuid, "issue_type", value, reload) - } - value={issue_type} - /> - - - - + + + set_report_issue_tracker_attribute(report_uuid, "type", value, reload)} + value={report.issue_tracker?.type} + /> + + + set_report_issue_tracker_attribute(report_uuid, "url", value, reload)} + value={report.issue_tracker?.parameters?.url} + /> + + + set_report_issue_tracker_attribute(report_uuid, "username", value, reload)} + value={report.issue_tracker?.parameters?.username} + /> + + + set_report_issue_tracker_attribute(report_uuid, "password", value, reload)} + value={report.issue_tracker?.parameters?.password} + /> + + + + set_report_issue_tracker_attribute(report_uuid, "private_token", value, reload) + } + value={report.issue_tracker?.parameters?.private_token} + /> + + + + + } + options={projectOptions} + placeholder="None" + set_value={(value) => set_report_issue_tracker_attribute(report_uuid, "project_key", value, reload)} + value={project_key} + /> + + + + } + options={issueTypeOptions} + placeholder="None" + set_value={(value) => set_report_issue_tracker_attribute(report_uuid, "issue_type", value, reload)} + value={issue_type} + /> + + + - - + title="Epic links not supported" + > + {`The issue type '${issue_type}' in project '${project_key}' does not support adding epic links when creating issues, so no epic link will be added to new issues.`} +
+
+
+ + - - + title="Labels not supported" + > + {`The issue type '${issue_type}' in project '${project_key}' does not support adding labels when creating issues, so no labels will be added to new issues.`} + + +
) } diff --git a/components/frontend/src/report/Report.js b/components/frontend/src/report/Report.js index 05db2beb3d..dc61a8c791 100644 --- a/components/frontend/src/report/Report.js +++ b/components/frontend/src/report/Report.js @@ -1,6 +1,6 @@ import { func } from "prop-types" -import { ExportCard } from "../dashboard/ExportCard" +import { PageHeader } from "../dashboard/PageHeader" import { datePropType, datesPropType, @@ -15,8 +15,8 @@ import { Subjects } from "../subject/Subjects" import { SubjectsButtonRow } from "../subject/SubjectsButtonRow" import { getReportTags } from "../utils" import { CommentSegment } from "../widgets/CommentSegment" +import { WarningMessage } from "../widgets/WarningMessage" import { ReportDashboard } from "./ReportDashboard" -import { ReportErrorMessage } from "./ReportErrorMessage" import { ReportTitle } from "./ReportTitle" export function Report({ @@ -42,12 +42,17 @@ export function Report({ } if (!report) { - return + return ( + + {report_date ? `Sorry, this report didn't exist at ${report_date}` : "Sorry, this report doesn't exist"} + + ) } // Sort measurements in reverse order so that if there multiple measurements on a day, we find the most recent one: const reversedMeasurements = measurements.slice().sort((m1, m2) => (m1.start < m2.start ? 1 : -1)) return (
+
-
- {children} - - ) -} -ErrorMessage.propTypes = { - children: string, -} - -export function ReportErrorMessage({ reportDate }) { - return ( - - {reportDate ? `Sorry, this report didn't exist at ${reportDate}` : "Sorry, this report doesn't exist"} - - ) -} -ReportErrorMessage.propTypes = { - reportDate: optionalDatePropType, -} - -export function ReportsOverviewErrorMessage({ reportDate }) { - return {`Sorry, no reports existed at ${reportDate}`} -} -ReportsOverviewErrorMessage.propTypes = { - reportDate: datePropType, -} diff --git a/components/frontend/src/report/ReportTitle.js b/components/frontend/src/report/ReportTitle.js index 8d4dfdbfa8..7bd77835a9 100644 --- a/components/frontend/src/report/ReportTitle.js +++ b/components/frontend/src/report/ReportTitle.js @@ -1,5 +1,6 @@ +import { Typography } from "@mui/material" +import Grid from "@mui/material/Grid2" import { bool, func, oneOfType, string } from "prop-types" -import { Grid } from "semantic-ui-react" import { delete_report, set_report_attribute } from "../api/report" import { activeTabIndex, tabChangeHandler } from "../app_ui_settings" @@ -10,7 +11,7 @@ import { IntegerInput } from "../fields/IntegerInput" import { StringInput } from "../fields/StringInput" import { STATUS_DESCRIPTION, STATUS_NAME, statusPropType } from "../metric/status" import { NotificationDestinations } from "../notification/NotificationDestinations" -import { Label, Segment, Tab } from "../semantic_ui_react_wrappers" +import { Tab } from "../semantic_ui_react_wrappers" import { entityStatusPropType, reportPropType, settingsPropType } from "../sharedPropTypes" import { SOURCE_ENTITY_STATUS_DESCRIPTION, SOURCE_ENTITY_STATUS_NAME } from "../source/source_entity_status" import { getDesiredResponseTime } from "../utils" @@ -25,36 +26,32 @@ import { IssueTracker } from "./IssueTracker" function ReportConfiguration({ reload, report }) { return ( - - - - set_report_attribute(report.report_uuid, "title", value, reload)} - value={report.title} - /> - - - set_report_attribute(report.report_uuid, "subtitle", value, reload)} - value={report.subtitle} - /> - - - - - set_report_attribute(report.report_uuid, "comment", value, reload)} - value={report.comment} - /> - - + + + set_report_attribute(report.report_uuid, "title", value, reload)} + value={report.title} + /> + + + set_report_attribute(report.report_uuid, "subtitle", value, reload)} + value={report.subtitle} + /> + + + set_report_attribute(report.report_uuid, "comment", value, reload)} + value={report.comment} + /> + ) } @@ -92,50 +89,40 @@ DesiredResponseTimeInput.propTypes = { function ReactionTimes(props) { return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + ) } ReactionTimes.propTypes = { diff --git a/components/frontend/src/report/ReportTitle.test.js b/components/frontend/src/report/ReportTitle.test.js index 64494f9548..84197a402c 100644 --- a/components/frontend/src/report/ReportTitle.test.js +++ b/components/frontend/src/report/ReportTitle.test.js @@ -84,7 +84,7 @@ it("sets the unknown status reaction time", async () => { await act(async () => { fireEvent.click(screen.getByText(/reaction times/)) }) - await userEvent.type(screen.getByLabelText(/Unknown/), "4{Enter}}", { + await userEvent.type(screen.getByLabelText("Unknown"), "4{Enter}}", { initialSelectionStart: 0, initialSelectionEnd: 1, }) @@ -102,7 +102,7 @@ it("sets the target not met status reaction time", async () => { await act(async () => { fireEvent.click(screen.getByText(/reaction times/)) }) - await userEvent.type(screen.getByLabelText(/Target not met/), "5{Enter}}", { + await userEvent.type(screen.getByLabelText("Target not met"), "5{Enter}}", { initialSelectionStart: 0, initialSelectionEnd: 1, }) @@ -120,7 +120,7 @@ it("sets the near target met status reaction time", async () => { await act(async () => { fireEvent.click(screen.getByText(/reaction times/)) }) - await userEvent.type(screen.getByLabelText(/Near target met/), "6{Enter}}", { + await userEvent.type(screen.getByLabelText("Near target met"), "6{Enter}}", { initialSelectionStart: 0, initialSelectionEnd: 2, }) @@ -156,7 +156,7 @@ it("sets the confirmed measurement entity status reaction time", async () => { await act(async () => { fireEvent.click(screen.getByText(/reaction times/)) }) - await userEvent.type(screen.getByLabelText(/Confirmed/), "60{Enter}}", { + await userEvent.type(screen.getByLabelText("Confirmed"), "60{Enter}}", { initialSelectionStart: 0, initialSelectionEnd: 3, }) @@ -174,7 +174,7 @@ it("sets the false positive measurement entity status reaction time", async () = await act(async () => { fireEvent.click(screen.getByText(/reaction times/)) }) - await userEvent.type(screen.getByLabelText(/False positive/), "70{Enter}}", { + await userEvent.type(screen.getByLabelText("False positive"), "70{Enter}}", { initialSelectionStart: 0, initialSelectionEnd: 3, }) @@ -192,7 +192,7 @@ it("sets the fixed measurement entity status reaction time", async () => { await act(async () => { fireEvent.click(screen.getByText(/reaction times/)) }) - await userEvent.type(screen.getByLabelText(/Fixed/), "80{Enter}}", { + await userEvent.type(screen.getByLabelText("Fixed"), "80{Enter}}", { initialSelectionStart: 0, initialSelectionEnd: 3, }) @@ -210,7 +210,7 @@ it("sets the won't fixed measurement entity status reaction time", async () => { await act(async () => { fireEvent.click(screen.getByText(/reaction times/)) }) - await userEvent.type(screen.getByLabelText(/Won't fix/), "90{Enter}}", { + await userEvent.type(screen.getByLabelText("Won't fix"), "90{Enter}}", { initialSelectionStart: 0, initialSelectionEnd: 3, }) diff --git a/components/frontend/src/report/ReportsOverview.js b/components/frontend/src/report/ReportsOverview.js index ff25b1ef57..dd38230501 100644 --- a/components/frontend/src/report/ReportsOverview.js +++ b/components/frontend/src/report/ReportsOverview.js @@ -3,7 +3,7 @@ import { func } from "prop-types" import { add_report, copy_report } from "../api/report" import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from "../context/Permissions" -import { ExportCard } from "../dashboard/ExportCard" +import { PageHeader } from "../dashboard/PageHeader" import { datePropType, datesPropType, @@ -21,7 +21,7 @@ import { AddButton } from "../widgets/buttons/AddButton" import { CopyButton } from "../widgets/buttons/CopyButton" import { CommentSegment } from "../widgets/CommentSegment" import { report_options } from "../widgets/menu_options" -import { ReportsOverviewErrorMessage } from "./ReportErrorMessage" +import { WarningMessage } from "../widgets/WarningMessage" import { ReportsOverviewDashboard } from "./ReportsOverviewDashboard" import { ReportsOverviewTitle } from "./ReportsOverviewTitle" @@ -63,20 +63,15 @@ export function ReportsOverview({ settings, }) { if (reports.length === 0 && report_date !== null) { - return + return {`Sorry, no reports existed at ${report_date}`} } // Sort measurements in reverse order so that if there multiple measurements on a day, we find the most recent one: const reversedMeasurements = measurements.slice().sort((m1, m2) => (m1.start < m2.start ? 1 : -1)) return (
+ -
{ const reports = [{ report_uuid: "report_uuid", subjects: {} }] const reportsOverview = { title: "Overview", permissions: {} } renderReportsOverview({ reports: reports, reportsOverview: reportsOverview }) - expect(screen.getAllByText(/Overview/).length).toBe(2) + expect(screen.getAllByText(/Overview/).length).toBe(1) }) it("shows the comment", async () => { diff --git a/components/frontend/src/report/ReportsOverviewTitle.js b/components/frontend/src/report/ReportsOverviewTitle.js index b5f383e1ee..a22f69108c 100644 --- a/components/frontend/src/report/ReportsOverviewTitle.js +++ b/components/frontend/src/report/ReportsOverviewTitle.js @@ -1,5 +1,5 @@ +import Grid from "@mui/material/Grid2" import { func, shape } from "prop-types" -import { Grid } from "semantic-ui-react" import { set_reports_attribute } from "../api/report" import { activeTabIndex, tabChangeHandler } from "../app_ui_settings" @@ -17,36 +17,32 @@ import { setDocumentTitle } from "./document_title" function ReportsOverviewConfiguration({ reports_overview, reload }) { return ( - - - - set_reports_attribute("title", value, reload)} - value={reports_overview.title} - /> - - - set_reports_attribute("subtitle", value, reload)} - value={reports_overview.subtitle} - /> - - - - - set_reports_attribute("comment", value, reload)} - value={reports_overview.comment} - /> - - + + + set_reports_attribute("title", value, reload)} + value={reports_overview.title} + /> + + + set_reports_attribute("subtitle", value, reload)} + value={reports_overview.subtitle} + /> + + + set_reports_attribute("comment", value, reload)} + value={reports_overview.comment} + /> + ) } @@ -62,35 +58,31 @@ function setPermissions(permissions, permission, value, reload) { function Permissions({ permissions, reload }) { return ( - - - - setPermissions(permissions, EDIT_REPORT_PERMISSION, value, reload)} - value={permissions[EDIT_REPORT_PERMISSION]} - /> - - - - - setPermissions(permissions, EDIT_ENTITY_PERMISSION, value, reload)} - value={permissions[EDIT_ENTITY_PERMISSION]} - /> - - + + + setPermissions(permissions, EDIT_REPORT_PERMISSION, value, reload)} + value={permissions[EDIT_REPORT_PERMISSION]} + /> + + + setPermissions(permissions, EDIT_ENTITY_PERMISSION, value, reload)} + value={permissions[EDIT_ENTITY_PERMISSION]} + /> + ) } diff --git a/components/frontend/src/semantic_ui_react_wrappers.js b/components/frontend/src/semantic_ui_react_wrappers.js index 53c7959114..cd42422e4e 100644 --- a/components/frontend/src/semantic_ui_react_wrappers.js +++ b/components/frontend/src/semantic_ui_react_wrappers.js @@ -3,8 +3,6 @@ export { Dropdown } from "./semantic_ui_react_wrappers/Dropdown" export { Form } from "./semantic_ui_react_wrappers/Form" export { Header } from "./semantic_ui_react_wrappers/Header" export { Label } from "./semantic_ui_react_wrappers/Label" -export { Message } from "./semantic_ui_react_wrappers/Message" -export { Popup } from "./semantic_ui_react_wrappers/Popup" export { Segment } from "./semantic_ui_react_wrappers/Segment" export { Tab } from "./semantic_ui_react_wrappers/Tab" export { Table } from "./semantic_ui_react_wrappers/Table" diff --git a/components/frontend/src/semantic_ui_react_wrappers/Message.js b/components/frontend/src/semantic_ui_react_wrappers/Message.js deleted file mode 100644 index dfdad0232a..0000000000 --- a/components/frontend/src/semantic_ui_react_wrappers/Message.js +++ /dev/null @@ -1,14 +0,0 @@ -import { useContext } from "react" -import { Message as SemanticUIMessage } from "semantic-ui-react" - -import { DarkMode } from "../context/DarkMode" -import { addInvertedClassNameWhenInDarkMode } from "./dark_mode" - -export function Message(props) { - return -} - -Message.Content = SemanticUIMessage.Content -Message.Header = SemanticUIMessage.Header -Message.Item = SemanticUIMessage.Item -Message.List = SemanticUIMessage.List diff --git a/components/frontend/src/semantic_ui_react_wrappers/Popup.css b/components/frontend/src/semantic_ui_react_wrappers/Popup.css deleted file mode 100644 index 750287c6ec..0000000000 --- a/components/frontend/src/semantic_ui_react_wrappers/Popup.css +++ /dev/null @@ -1,14 +0,0 @@ -.ui.inverted.popup { - background-color: rgba(60, 65, 70); - box-shadow: - 0 2px 4px 0 rgba(255, 255, 255, 0.1), - 0 2px 8px 0 rgba(255, 255, 255, 0.15); -} - -.ui.inverted.popup .negative.message .header { - color: #912d2b; /* For some reason the header color is white within an inverted popup. Override. */ -} - -.ui.inverted.popup:before { - background-color: rgba(60, 65, 70) !important; -} diff --git a/components/frontend/src/semantic_ui_react_wrappers/Popup.js b/components/frontend/src/semantic_ui_react_wrappers/Popup.js deleted file mode 100644 index e5cdcacbbc..0000000000 --- a/components/frontend/src/semantic_ui_react_wrappers/Popup.js +++ /dev/null @@ -1,12 +0,0 @@ -import "./Popup.css" - -import { useContext } from "react" -import { Popup as SemanticUIPopup } from "semantic-ui-react" - -import { DarkMode } from "../context/DarkMode" - -export function Popup(props) { - return -} - -Popup.Content = SemanticUIPopup.Content diff --git a/components/frontend/src/source/Source.js b/components/frontend/src/source/Source.js index 9bee36716a..998bf5b813 100644 --- a/components/frontend/src/source/Source.js +++ b/components/frontend/src/source/Source.js @@ -1,6 +1,6 @@ +import Grid from "@mui/material/Grid2" import { bool, func, object, oneOfType, string } from "prop-types" import { useContext } from "react" -import { Grid } from "semantic-ui-react" import { delete_source, set_source_attribute } from "../api/source" import { ChangeLog } from "../changelog/ChangeLog" @@ -73,39 +73,35 @@ function Parameters({ const dataModel = useContext(DataModel) const source_type = dataModel.sources[source.type] return ( - - - - set_source_attribute(source_uuid, a, v, reload)} - source_uuid={source_uuid} - source_type={source.type} - /> - - - set_source_attribute(source_uuid, "name", value, reload)} - value={source.name} - /> - - - - - - - + + + set_source_attribute(source_uuid, a, v, reload)} + source_uuid={source_uuid} + source_type={source.type} + /> + + + set_source_attribute(source_uuid, "name", value, reload)} + value={source.name} + /> + + + + {connection_error && } {parse_error && } {config_error && } diff --git a/components/frontend/src/source/SourceEntities.js b/components/frontend/src/source/SourceEntities.js index 15b44f2447..000e99464f 100644 --- a/components/frontend/src/source/SourceEntities.js +++ b/components/frontend/src/source/SourceEntities.js @@ -4,10 +4,9 @@ import HelpIcon from "@mui/icons-material/Help" import { IconButton, Tooltip } from "@mui/material" import { bool, func, object, string } from "prop-types" import { useContext, useState } from "react" -import { Message } from "semantic-ui-react" import { DataModel } from "../context/DataModel" -import { Popup, Table } from "../semantic_ui_react_wrappers" +import { Table } from "../semantic_ui_react_wrappers" import { alignmentPropType, childrenPropType, @@ -25,7 +24,7 @@ import { import { capitalize } from "../utils" import { IgnoreIcon, ShowIcon } from "../widgets/icons" import { LoadingPlaceHolder } from "../widgets/Placeholder" -import { FailedToLoadMeasurementsWarningMessage } from "../widgets/WarningMessage" +import { FailedToLoadMeasurementsWarningMessage, InfoMessage } from "../widgets/WarningMessage" import { SourceEntity } from "./SourceEntity" function entityStatus(source, entity) { @@ -144,16 +143,12 @@ function EntityAttributeHeaderCell({ entityAttribute, ...sortProps }) { > {entityAttribute.name} {entityAttribute.help ? ( - -   - - - } - content={entityAttribute.help} - /> + + +   + + + ) : null} ) @@ -270,10 +265,9 @@ export function SourceEntities({ loading, measurements, metric, metric_uuid, rel const unit = dataModel.metrics[metric.type].unit || "entities" const sourceTypeName = dataModel.sources[sourceType].name return ( - + + {`Showing individual ${unit} is not supported when using ${sourceTypeName} as source.`} + ) } if (loading === "failed") { @@ -284,20 +278,18 @@ export function SourceEntities({ loading, measurements, metric, metric_uuid, rel } if (measurements.length === 0) { return ( - + + Measurement details not available because Quality-time has not collected any measurements yet. + ) } const lastMeasurement = measurements[measurements.length - 1] const source = lastMeasurement.sources.find((source) => source.source_uuid === source_uuid) if (!Array.isArray(source.entities) || source.entities.length === 0) { return ( - + + There are currently no measurement details available. + ) } const entityAttributes = metricEntities.attributes.filter((attribute) => attribute?.visible ?? true) diff --git a/components/frontend/src/source/SourceEntities.test.js b/components/frontend/src/source/SourceEntities.test.js index 98cf824b89..844d0b14fb 100644 --- a/components/frontend/src/source/SourceEntities.test.js +++ b/components/frontend/src/source/SourceEntities.test.js @@ -131,7 +131,7 @@ it("renders a message if the metric does not support measurement entities", () = ).toBe(1) }) -it("renders a message if the metric does not support measurement entities andhas no unit", () => { +it("renders a message if the metric does not support measurement entities and has no unit", () => { renderSourceEntities({ metric: { type: "metric_type_without_unit", diff --git a/components/frontend/src/source/SourceEntityDetails.js b/components/frontend/src/source/SourceEntityDetails.js index 632babc659..f90240114d 100644 --- a/components/frontend/src/source/SourceEntityDetails.js +++ b/components/frontend/src/source/SourceEntityDetails.js @@ -1,5 +1,6 @@ +import Grid from "@mui/material/Grid2" import { func, node, oneOf, string } from "prop-types" -import { Grid, Header } from "semantic-ui-react" +import { Header } from "semantic-ui-react" import { set_source_entity_attribute } from "../api/source" import { EDIT_ENTITY_PERMISSION } from "../context/Permissions" @@ -77,65 +78,56 @@ export function SourceEntityDetails({ source_uuid, }) { return ( - - - - - set_source_entity_attribute(metric_uuid, source_uuid, entity.key, "status", value, reload) - } - value={status} - sort={false} - /> - - - - } - placeholder="YYYY-MM-DD" - set_value={(value) => - set_source_entity_attribute( - metric_uuid, - source_uuid, - entity.key, - "status_end_date", - value, - reload, - ) - } - value={status_end_date} - /> - - - - set_source_entity_attribute( - metric_uuid, - source_uuid, - entity.key, - "rationale", - value, - reload, - ) - } - value={rationale} - /> - - + + + + set_source_entity_attribute(metric_uuid, source_uuid, entity.key, "status", value, reload) + } + value={status} + sort={false} + /> + + + + } + placeholder="YYYY-MM-DD" + set_value={(value) => + set_source_entity_attribute( + metric_uuid, + source_uuid, + entity.key, + "status_end_date", + value, + reload, + ) + } + value={status_end_date} + /> + + + + set_source_entity_attribute(metric_uuid, source_uuid, entity.key, "rationale", value, reload) + } + value={rationale} + /> + ) } diff --git a/components/frontend/src/source/SourceParameters.js b/components/frontend/src/source/SourceParameters.js index fc3ac78908..bdd9892f79 100644 --- a/components/frontend/src/source/SourceParameters.js +++ b/components/frontend/src/source/SourceParameters.js @@ -1,6 +1,6 @@ +import Grid from "@mui/material/Grid2" import { func, string } from "prop-types" import { useContext } from "react" -import { Grid } from "semantic-ui-react" import { DataModel } from "../context/DataModel" import { EDIT_REPORT_PERMISSION } from "../context/Permissions" @@ -76,19 +76,19 @@ export function SourceParameters({ changed_param_keys, metric, reload, report, s
)) return ( - +
{parameterGroup.name}
{parameters}
-
+ ) }) return ( - - {groups} + + {groups} ) } diff --git a/components/frontend/src/source/Sources.js b/components/frontend/src/source/Sources.js index 15a6a3b9a3..4e3064e848 100644 --- a/components/frontend/src/source/Sources.js +++ b/components/frontend/src/source/Sources.js @@ -4,7 +4,7 @@ import { useContext } from "react" import { add_source, copy_source, move_source } from "../api/source" import { DataModel } from "../context/DataModel" import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from "../context/Permissions" -import { Message, Segment } from "../semantic_ui_react_wrappers" +import { Segment } from "../semantic_ui_react_wrappers" import { measurementPropType, measurementSourcePropType, @@ -20,6 +20,7 @@ import { CopyButton } from "../widgets/buttons/CopyButton" import { MoveButton } from "../widgets/buttons/MoveButton" import { source_options } from "../widgets/menu_options" import { showMessage } from "../widgets/toast" +import { InfoMessage } from "../widgets/WarningMessage" import { Source } from "./Source" import { sourceTypeOptions } from "./SourceType" @@ -118,10 +119,7 @@ export function Sources({ reports, report, metric, metric_uuid, measurement, cha return ( <> {sourceSegments.length === 0 ? ( - - No sources -

No sources have been configured yet.

-
+ No sources have been configured yet. ) : ( sourceSegments )} diff --git a/components/frontend/src/subject/SubjectParameters.js b/components/frontend/src/subject/SubjectParameters.js index ec40812d73..e923df7481 100644 --- a/components/frontend/src/subject/SubjectParameters.js +++ b/components/frontend/src/subject/SubjectParameters.js @@ -1,5 +1,5 @@ +import Grid from "@mui/material/Grid2" import { func, string } from "prop-types" -import { Grid } from "semantic-ui-react" import { set_subject_attribute } from "../api/subject" import { EDIT_REPORT_PERMISSION } from "../context/Permissions" @@ -10,44 +10,40 @@ import { SubjectType } from "./SubjectType" export function SubjectParameters({ subject, subject_uuid, subject_name, reload }) { return ( - - - - set_subject_attribute(subject_uuid, "type", value, reload)} - subjectType={subject.type} - /> - - - set_subject_attribute(subject_uuid, "name", value, reload)} - value={subject.name} - /> - - - set_subject_attribute(subject_uuid, "subtitle", value, reload)} - value={subject.subtitle} - /> - - - - - set_subject_attribute(subject_uuid, "comment", value, reload)} - value={subject.comment} - /> - - + + + set_subject_attribute(subject_uuid, "type", value, reload)} + subjectType={subject.type} + /> + + + set_subject_attribute(subject_uuid, "name", value, reload)} + value={subject.name} + /> + + + set_subject_attribute(subject_uuid, "subtitle", value, reload)} + value={subject.subtitle} + /> + + + set_subject_attribute(subject_uuid, "comment", value, reload)} + value={subject.comment} + /> + ) } diff --git a/components/frontend/src/subject/SubjectTableRow.js b/components/frontend/src/subject/SubjectTableRow.js index d338f4a430..0b22e8b2f0 100644 --- a/components/frontend/src/subject/SubjectTableRow.js +++ b/components/frontend/src/subject/SubjectTableRow.js @@ -1,3 +1,4 @@ +import { Tooltip } from "@mui/material" import { bool, func, number, object, string } from "prop-types" import { useContext } from "react" @@ -12,7 +13,7 @@ import { StatusIcon } from "../measurement/StatusIcon" import { TimeLeft } from "../measurement/TimeLeft" import { TrendSparkline } from "../measurement/TrendSparkline" import { MetricDetails } from "../metric/MetricDetails" -import { Label, Popup, Table } from "../semantic_ui_react_wrappers" +import { Label, Table } from "../semantic_ui_react_wrappers" import { dataModelPropType, datePropType, @@ -131,14 +132,13 @@ function DeltaCell({ dateOrderAscending, index, metric, metricValue, previousVal const description = deltaDescription(dataModel, metric, scale, delta, improved, oldValue, newValue) const color = deltaColor(metric, improved) label = ( - + + + - } - /> + + ) } return ( diff --git a/components/frontend/src/utils.js b/components/frontend/src/utils.js index c70554c64c..ef1cb76eb3 100644 --- a/components/frontend/src/utils.js +++ b/components/frontend/src/utils.js @@ -19,7 +19,6 @@ export const MILLISECONDS_PER_HOUR = 60 * 60 * 1000 const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR export const ISSUE_STATUS_COLORS = { todo: "grey", doing: "blue", done: "green", unknown: null } -export const ISSUE_STATUS_THEME_COLORS = { todo: "grey", doing: "info", done: "success", unknown: "" } export function getMetricDirection(metric, dataModel) { // Old versions of the data model may contain the unicode version of the direction, be prepared: diff --git a/components/frontend/src/widgets/CommentSegment.js b/components/frontend/src/widgets/CommentSegment.js index 45526d3137..58a1dd8fd6 100644 --- a/components/frontend/src/widgets/CommentSegment.js +++ b/components/frontend/src/widgets/CommentSegment.js @@ -1,13 +1,12 @@ +import { Box } from "@mui/material" import { string } from "prop-types" -import { Segment } from "../semantic_ui_react_wrappers" - export function CommentSegment({ comment }) { if (comment) { return ( - +
- + ) } return null diff --git a/components/frontend/src/widgets/Label.js b/components/frontend/src/widgets/Label.js new file mode 100644 index 0000000000..5a65379d4a --- /dev/null +++ b/components/frontend/src/widgets/Label.js @@ -0,0 +1,27 @@ +import { Box } from "@mui/material" +import { string } from "prop-types" + +import { childrenPropType } from "../sharedPropTypes" + +export function Label({ color, children }) { + const bgcolor = `${color}.main` + const fgcolor = `${color}.contrastText` + return ( + + {children} + + ) +} +Label.propTypes = { + color: string, + children: childrenPropType, +} diff --git a/components/frontend/src/widgets/LabelWithHelp.js b/components/frontend/src/widgets/LabelWithHelp.js index 1d744eb063..48febb721e 100644 --- a/components/frontend/src/widgets/LabelWithHelp.js +++ b/components/frontend/src/widgets/LabelWithHelp.js @@ -1,20 +1,16 @@ import HelpIcon from "@mui/icons-material/Help" -import { bool, string } from "prop-types" +import { Tooltip } from "@mui/material" +import { string } from "prop-types" -import { Popup } from "../semantic_ui_react_wrappers" import { labelPropType, popupContentPropType } from "../sharedPropTypes" -export function LabelWithHelp({ labelId, labelFor, label, help, hoverable }) { +export function LabelWithHelp({ labelId, labelFor, label, help }) { return ( ) } @@ -23,5 +19,4 @@ LabelWithHelp.propTypes = { labelFor: string, label: labelPropType, help: popupContentPropType, - hoverable: bool, } diff --git a/components/frontend/src/widgets/WarningMessage.js b/components/frontend/src/widgets/WarningMessage.js index 86dea71c07..18de1cfe31 100644 --- a/components/frontend/src/widgets/WarningMessage.js +++ b/components/frontend/src/widgets/WarningMessage.js @@ -1,21 +1,40 @@ -import { bool } from "prop-types" +import { Alert, AlertTitle } from "@mui/material" +import { bool, string } from "prop-types" -import { Message } from "../semantic_ui_react_wrappers" +import { childrenPropType } from "../sharedPropTypes" -export function WarningMessage(props) { +export function WarningMessage({ children, title, showIf }) { // Show a warning message if showIf is true or undefined - const { showIf, ...messageProps } = props - return (showIf ?? true) ? : null + return (showIf ?? true) ? ( + + {title} + {children} + + ) : null } WarningMessage.propTypes = { + children: childrenPropType, showIf: bool, + title: string, } export function FailedToLoadMeasurementsWarningMessage() { return ( - + + Loading the measurements from the API-server failed. + ) } + +export function InfoMessage({ children, title }) { + return ( + + {title} + {children} + + ) +} +InfoMessage.propTypes = { + children: childrenPropType, + title: string, +} diff --git a/components/frontend/src/widgets/WarningMessage.test.js b/components/frontend/src/widgets/WarningMessage.test.js index bc859c756f..cfc5fb9ab7 100644 --- a/components/frontend/src/widgets/WarningMessage.test.js +++ b/components/frontend/src/widgets/WarningMessage.test.js @@ -3,16 +3,16 @@ import { render, screen } from "@testing-library/react" import { WarningMessage } from "./WarningMessage" it("shows a warning message if showIf is true", () => { - render() + render(Warning) expect(screen.getAllByText("Warning").length).toBe(1) }) it("does not show a warning message if showIf is false", () => { - render() + render(Warning) expect(screen.queryAllByText("Warning").length).toBe(0) }) it("shows a warning message if showIf is undefined", () => { - render() + render(Warning) expect(screen.getAllByText("Warning").length).toBe(1) }) diff --git a/components/frontend/src/widgets/icons.js b/components/frontend/src/widgets/icons.js index 3d5fcb39cf..55fa0fe45f 100644 --- a/components/frontend/src/widgets/icons.js +++ b/components/frontend/src/widgets/icons.js @@ -42,7 +42,7 @@ export function DeleteItemIcon() { } export function IgnoreIcon() { - return + return } export function MoveItemIcon() {