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},
+ }
+}