diff --git a/components/frontend/src/metric/MetricDetails.js b/components/frontend/src/metric/MetricDetails.js index 95f060999c..81a772ff83 100644 --- a/components/frontend/src/metric/MetricDetails.js +++ b/components/frontend/src/metric/MetricDetails.js @@ -1,6 +1,5 @@ import { bool, func, string } from "prop-types" import { useContext, useEffect, useState } from "react" -import { Icon, Menu } from "semantic-ui-react" import { get_metric_measurements } from "../api/measurement" import { delete_metric, set_metric_attribute } from "../api/metric" @@ -8,7 +7,7 @@ import { activeTabIndex, tabChangeHandler } from "../app_ui_settings" import { ChangeLog } from "../changelog/ChangeLog" import { DataModel } from "../context/DataModel" import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from "../context/Permissions" -import { Label, Tab } from "../semantic_ui_react_wrappers" +import { Tab } from "../semantic_ui_react_wrappers" import { datePropType, metricPropType, @@ -21,7 +20,8 @@ import { SourceEntities } from "../source/SourceEntities" import { Sources } from "../source/Sources" import { getMetricScale, getSourceName, isMeasurementRequested } from "../utils" import { ActionButton, DeleteButton, PermLinkButton, ReorderButtonGroup } from "../widgets/Button" -import { FocusableTab } from "../widgets/FocusableTab" +import { changelogTab, configurationTab, focusableTab } from "../widgets/FocusableTab" +import { tabPane } from "../widgets/TabPane" import { showMessage } from "../widgets/toast" import { MetricConfigurationParameters } from "./MetricConfigurationParameters" import { MetricDebtParameters } from "./MetricDebtParameters" @@ -130,96 +130,45 @@ export function MetricDetails({ Object.values(metric.sources ?? {}).some( (source) => !dataModel.metrics[metric.type].sources.includes(source.type), ) - const sources_menu_item = any_error ? : "Sources" const metricUrl = `${window.location.href.split("#")[0]}#${metric_uuid}` let panes = [] panes.push( - { - menuItem: ( - - - {"Configuration"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Technical debt"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {sources_menu_item} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Changelog"} - - ), - render: () => ( - - - - ), - }, + tabPane( + configurationTab(), + , + ), + tabPane( + focusableTab("Technical debt", "money"), + , + ), + tabPane( + focusableTab("Sources", "server", Boolean(any_error)), + , + ), + tabPane(changelogTab(), ), ) if (measurements.length > 0) { if (getMetricScale(metric, dataModel) !== "version_number") { - panes.push({ - menuItem: ( - - - {"Trend graph"} - - ), - render: () => ( - - - + panes.push( + tabPane( + focusableTab("Trend graph", "linegraph"), + , ), - }) + ) } last_measurement.sources.forEach((source) => { const report_source = metric.sources[source.source_uuid] @@ -231,24 +180,18 @@ export function MetricDetails({ return } // no entities to show, continue const source_name = getSourceName(report_source, dataModel) - panes.push({ - menuItem: ( - - {source_name} - - ), - render: () => ( - - - + panes.push( + tabPane( + focusableTab(source_name), + , ), - }) + ) }) } diff --git a/components/frontend/src/report/ReportTitle.js b/components/frontend/src/report/ReportTitle.js index 66ce3d7335..5fe0d04037 100644 --- a/components/frontend/src/report/ReportTitle.js +++ b/components/frontend/src/report/ReportTitle.js @@ -1,5 +1,5 @@ import { func, string } from "prop-types" -import { Grid, Icon, Menu } from "semantic-ui-react" +import { Grid } from "semantic-ui-react" import { delete_report, set_report_attribute } from "../api/report" import { activeTabIndex, tabChangeHandler } from "../app_ui_settings" @@ -14,9 +14,10 @@ import { Label, Segment, Tab } from "../semantic_ui_react_wrappers" import { reportPropType, settingsPropType } from "../sharedPropTypes" import { getDesiredResponseTime } from "../utils" import { DeleteButton, PermLinkButton } from "../widgets/Button" -import { FocusableTab } from "../widgets/FocusableTab" +import { changelogTab, configurationTab, focusableTab } from "../widgets/FocusableTab" import { HeaderWithDetails } from "../widgets/HeaderWithDetails" import { LabelWithHelp } from "../widgets/LabelWithHelp" +import { tabPane } from "../widgets/TabPane" import { setDocumentTitle } from "./document_title" import { IssueTracker } from "./IssueTracker" @@ -313,75 +314,18 @@ export function ReportTitle({ report, openReportsOverview, reload, settings }) { const tabIndex = activeTabIndex(settings.expandedItems, report_uuid) const reportUrl = `${window.location}` const panes = [ - { - menuItem: ( - - - {"Configuration"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Desired reaction times"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Notifications"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Issue tracker"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Changelog"} - - ), - render: () => ( - - - - ), - }, + tabPane(configurationTab(), ), + tabPane(focusableTab("Desired reaction times", "time"), ), + tabPane( + focusableTab("Notifications", "feed"), + , + ), + tabPane(focusableTab("Issue tracker", "tasks"), ), + tabPane(changelogTab(), ), ] setDocumentTitle(report.title) diff --git a/components/frontend/src/report/ReportsOverviewTitle.js b/components/frontend/src/report/ReportsOverviewTitle.js index cb583768ca..0c221140be 100644 --- a/components/frontend/src/report/ReportsOverviewTitle.js +++ b/components/frontend/src/report/ReportsOverviewTitle.js @@ -1,5 +1,5 @@ import { func, shape } from "prop-types" -import { Grid, Icon, Menu } from "semantic-ui-react" +import { Grid } from "semantic-ui-react" import { set_reports_attribute } from "../api/report" import { activeTabIndex, tabChangeHandler } from "../app_ui_settings" @@ -11,8 +11,9 @@ import { StringInput } from "../fields/StringInput" import { Tab } from "../semantic_ui_react_wrappers" import { permissionsPropType, reportsOverviewPropType, settingsPropType } from "../sharedPropTypes" import { dropdownOptions } from "../utils" -import { FocusableTab } from "../widgets/FocusableTab" +import { changelogTab, configurationTab, focusableTab } from "../widgets/FocusableTab" import { HeaderWithDetails } from "../widgets/HeaderWithDetails" +import { tabPane } from "../widgets/TabPane" import { setDocumentTitle } from "./document_title" function ReportsOverviewConfiguration({ reports_overview, reload }) { @@ -106,45 +107,15 @@ export function ReportsOverviewTitle({ reports_overview, reload, settings }) { const uuid = "reports_overview" const tabIndex = activeTabIndex(settings.expandedItems, uuid) const panes = [ - { - menuItem: ( - - - {"Configuration"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Permissions"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Changelog"} - - ), - render: () => ( - - - - ), - }, + tabPane( + configurationTab(), + , + ), + tabPane( + focusableTab("Permissions", "lock"), + , + ), + tabPane(changelogTab(), ), ] setDocumentTitle(reports_overview.title) diff --git a/components/frontend/src/source/Source.js b/components/frontend/src/source/Source.js index 99232b1bd1..d886a8f7a7 100644 --- a/components/frontend/src/source/Source.js +++ b/components/frontend/src/source/Source.js @@ -1,6 +1,6 @@ import { bool, func, object, oneOfType, string } from "prop-types" import { useContext } from "react" -import { Grid, Menu } from "semantic-ui-react" +import { Grid } from "semantic-ui-react" import { delete_source, set_source_attribute } from "../api/source" import { ChangeLog } from "../changelog/ChangeLog" @@ -8,7 +8,7 @@ import { DataModel } from "../context/DataModel" import { EDIT_REPORT_PERMISSION, ReadOnlyOrEditable } from "../context/Permissions" import { ErrorMessage } from "../errorMessage" import { StringInput } from "../fields/StringInput" -import { Icon, Label, Tab } from "../semantic_ui_react_wrappers" +import { Tab } from "../semantic_ui_react_wrappers" import { measurementSourcePropType, metricPropType, @@ -18,8 +18,9 @@ import { } from "../sharedPropTypes" import { getMetricName, getSourceName } from "../utils" import { DeleteButton, ReorderButtonGroup } from "../widgets/Button" -import { FocusableTab } from "../widgets/FocusableTab" +import { changelogTab, configurationTab } from "../widgets/FocusableTab" import { HyperLink } from "../widgets/HyperLink" +import { tabPane } from "../widgets/TabPane" import { SourceParameters } from "./SourceParameters" import { SourceType } from "./SourceType" import { SourceTypeHeader } from "./SourceTypeHeader" @@ -171,45 +172,22 @@ export function Source({ ) const configError = dataModel.metrics[metric.type].sources.includes(source.type) ? "" : configErrorMessage - const configurationTabLabel = - configError || connectionError || parseError ? : "Configuration" const panes = [ - { - menuItem: ( - - - {configurationTabLabel} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Changelog"} - - ), - render: () => ( - - - - ), - }, + tabPane( + configurationTab(Boolean(configError || connectionError || parseError)), + , + ), + tabPane(changelogTab(), ), ] return ( <> diff --git a/components/frontend/src/subject/SubjectTitle.js b/components/frontend/src/subject/SubjectTitle.js index 54281f8684..0b0608b5a0 100644 --- a/components/frontend/src/subject/SubjectTitle.js +++ b/components/frontend/src/subject/SubjectTitle.js @@ -1,6 +1,6 @@ import { bool, func, object, string } from "prop-types" import { useContext } from "react" -import { Icon, Menu } from "semantic-ui-react" +import { Icon } from "semantic-ui-react" import { delete_subject, set_subject_attribute } from "../api/subject" import { activeTabIndex, tabChangeHandler } from "../app_ui_settings" @@ -11,9 +11,10 @@ import { Header, Tab } from "../semantic_ui_react_wrappers" import { reportPropType, settingsPropType } from "../sharedPropTypes" import { getSubjectType, slugify } from "../utils" import { DeleteButton, PermLinkButton, ReorderButtonGroup } from "../widgets/Button" -import { FocusableTab } from "../widgets/FocusableTab" +import { changelogTab, configurationTab } from "../widgets/FocusableTab" import { HeaderWithDetails } from "../widgets/HeaderWithDetails" import { HyperLink } from "../widgets/HyperLink" +import { tabPane } from "../widgets/TabPane" import { SubjectParameters } from "./SubjectParameters" function SubjectHeader({ subjectType }) { @@ -82,37 +83,16 @@ export function SubjectTitle({ const subjectTitle = (atReportsOverview ? report.title + " ❯ " : "") + subjectName const subjectUrl = `${window.location}#${subject_uuid}` const panes = [ - { - menuItem: ( - - - {"Configuration"} - - ), - render: () => ( - - - - ), - }, - { - menuItem: ( - - - {"Changelog"} - - ), - render: () => ( - - - - ), - }, + tabPane( + configurationTab(), + , + ), + tabPane(changelogTab(), ), ] return ( diff --git a/components/frontend/src/widgets/FocusableTab.js b/components/frontend/src/widgets/FocusableTab.js index 1e156b86a4..1ac65161c3 100644 --- a/components/frontend/src/widgets/FocusableTab.js +++ b/components/frontend/src/widgets/FocusableTab.js @@ -1,14 +1,53 @@ import "./FocusableTab.css" +import { bool, element, oneOfType, string } from "prop-types" import { useContext } from "react" +import { Icon, Menu } from "semantic-ui-react" import { DarkMode } from "../context/DarkMode" -import { childrenPropType } from "../sharedPropTypes" +import { Label } from "../semantic_ui_react_wrappers" -export function FocusableTab(props) { +function FocusableTabIcon({ iconName }) { + if (iconName === "linegraph") { + /* Using name="linegraph" results in "Invalid prop `name` of value `linegraph` supplied to `Icon`." + Using name="line graph" does not show the icon. Using className works around this. */ + return + } + return +} +FocusableTabIcon.propTypes = { + iconName: string, +} + +function FocusableTab({ error, iconName, label }) { const className = useContext(DarkMode) ? "tabbutton inverted" : "tabbutton" - return + const tabLabel = error ? : label + return ( + <> + {iconName && } + + + ) } FocusableTab.propTypes = { - children: childrenPropType, + error: bool, + iconName: string, + label: oneOfType([element, string]), +} + +export function focusableTab(label, iconName, error) { + // Return a Menu.Item to be used as menuItem property for a Tab. + return ( + + + + ) +} + +export function configurationTab(error) { + return focusableTab("Configuration", "settings", error) +} + +export function changelogTab() { + return focusableTab("Changelog", "history") } diff --git a/components/frontend/src/widgets/FocusableTab.test.js b/components/frontend/src/widgets/FocusableTab.test.js index c0bfbedfe2..50e1332aa2 100644 --- a/components/frontend/src/widgets/FocusableTab.test.js +++ b/components/frontend/src/widgets/FocusableTab.test.js @@ -1,18 +1,14 @@ import { render, screen } from "@testing-library/react" import { DarkMode } from "../context/DarkMode" -import { FocusableTab } from "./FocusableTab" +import { focusableTab } from "./FocusableTab" it("shows the tab", () => { - render(Tab) + render({focusableTab("Tab")}) expect(screen.queryAllByText("Tab").length).toBe(1) }) it("is inverted in dark mode", () => { - const { container } = render( - - Tab - , - ) - expect(container.firstChild.className).toEqual(expect.stringContaining("inverted")) + const { container } = render({focusableTab("Tab")}) + expect(container.firstChild.firstChild.className).toEqual(expect.stringContaining("inverted")) }) diff --git a/components/frontend/src/widgets/TabPane.js b/components/frontend/src/widgets/TabPane.js new file mode 100644 index 0000000000..3d1ce5f755 --- /dev/null +++ b/components/frontend/src/widgets/TabPane.js @@ -0,0 +1,8 @@ +import { Tab } from "../semantic_ui_react_wrappers" + +export function tabPane(tab, pane) { + return { + menuItem: tab, + render: () => {pane}, + } +}