Skip to content

Commit

Permalink
Always show measurement entity tabs.
Browse files Browse the repository at this point in the history
Display a loader in the tab while loading measurements. If the measurement entities cannot be displayed, explain in the tab why not.

Closes #8826.
  • Loading branch information
fniessink committed Jul 19, 2024
1 parent 9527ce8 commit 3b8605d
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 56 deletions.
44 changes: 18 additions & 26 deletions components/frontend/src/metric/MetricDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,32 +171,24 @@ export function MetricDetails({
{ iconName: "linegraph" },
),
)
if (measurements.length > 0) {
lastMeasurement.sources.forEach((source) => {
const reportSource = metric.sources[source.source_uuid]
if (!reportSource) {
return
} // source was deleted, continue
const nrEntities = source.entities?.length ?? 0
if (nrEntities === 0) {
return
} // no entities to show, continue
const sourceName = getSourceName(reportSource, dataModel)
panes.push(
tabPane(
sourceName,
<SourceEntities
report={report}
metric={metric}
metric_uuid={metric_uuid}
source={source}
reload={measurementsReload}
/>,
{ image: <Logo logo={reportSource.type} alt={sourceName} /> },
),
)
})
}
Object.entries(metric.sources).forEach(([source_uuid, source]) => {
const sourceName = getSourceName(source, dataModel)
panes.push(
tabPane(
sourceName,
<SourceEntities
loading={measurementsStatus}
measurements={measurements}
metric={metric}
metric_uuid={metric_uuid}
reload={measurementsReload}
report={report}
source_uuid={source_uuid}
/>,
{ image: <Logo logo={source.type} alt={sourceName} /> },
),
)
})

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion components/frontend/src/metric/MetricDetails.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ async function renderMetricDetails(stopFilteringAndSorting, connection_error, fa
end: "2020-02-29T11:25:52.252Z",
sources: [
{},
{ source_uuid: "source_uuid" },
{ source_uuid: "source_uuid2" },
{
source_uuid: "source_uuid",
entities: [{ key: "1" }],
Expand Down
25 changes: 8 additions & 17 deletions components/frontend/src/metric/TrendGraph.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { oneOf } from "prop-types"
import { useContext } from "react"
import { Placeholder, PlaceholderImage } from "semantic-ui-react"
import { Message } from "semantic-ui-react"
import { VictoryAxis, VictoryChart, VictoryLabel, VictoryLine, VictoryTheme } from "victory"

import { DarkMode } from "../context/DarkMode"
import { DataModel } from "../context/DataModel"
import { measurementsPropType, metricPropType } from "../sharedPropTypes"
import { loadingPropType, measurementsPropType, metricPropType } from "../sharedPropTypes"
import { capitalize, formatMetricScaleAndUnit, getMetricName, getMetricScale, niceNumber, scaledNumber } from "../utils"
import { WarningMessage } from "../widgets/WarningMessage"
import { LoadingPlaceHolder } from "../widgets/Placeholder"
import { FailedToLoadMeasurementsWarningMessage, WarningMessage } from "../widgets/WarningMessage"

function measurementAttributeAsNumber(metric, measurement, field, dataModel) {
const scale = getMetricScale(metric, dataModel)
Expand All @@ -22,26 +22,17 @@ 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 (
<WarningMessage
<Message
content="Trend graphs are not supported for metrics with a version number scale."
header="Trend graph not supported for version numbers"
/>
)
}
if (loading === "failed") {
return (
<WarningMessage
content="Loading the measurements from the API-server failed."
header="Loading measurements failed"
/>
)
return <FailedToLoadMeasurementsWarningMessage />
}
if (loading === "loading") {
return (
<Placeholder fluid inverted={darkMode} style={{ height: estimatedTotalChartHeight }}>
<PlaceholderImage />
</Placeholder>
)
return <LoadingPlaceHolder height={estimatedTotalChartHeight} />
}
if (measurements.length === 0) {
return (
Expand Down Expand Up @@ -117,7 +108,7 @@ export function TrendGraph({ metric, measurements, loading }) {
)
}
TrendGraph.propTypes = {
loading: oneOf(["failed", "loaded", "loading"]),
loading: loadingPropType,
metric: metricPropType,
measurements: measurementsPropType,
}
2 changes: 2 additions & 0 deletions components/frontend/src/sharedPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export const sortDirectionURLSearchQueryPropType = shape({
value: sortDirectionPropType,
})

export const loadingPropType = oneOf(["failed", "loaded", "loading"])

export const hiddenCardsPropType = oneOf(["action_required", "reports", "subjects", "tags", "issues", "legend"])

export const metricsToHidePropType = oneOf(["all", "none", "no_action_required", "no_issues"])
Expand Down
51 changes: 45 additions & 6 deletions components/frontend/src/source/SourceEntities.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "./SourceEntities.css"

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 { Button, Icon, Popup, Table } from "../semantic_ui_react_wrappers"
Expand All @@ -12,12 +13,16 @@ import {
entityAttributesPropType,
entityAttributeTypePropType,
entityPropType,
loadingPropType,
measurementsPropType,
metricPropType,
reportPropType,
sortDirectionPropType,
sourcePropType,
} from "../sharedPropTypes"
import { capitalize } from "../utils"
import { LoadingPlaceHolder } from "../widgets/Placeholder"
import { FailedToLoadMeasurementsWarningMessage } from "../widgets/WarningMessage"
import { SourceEntity } from "./SourceEntity"

function entityStatus(source, entity) {
Expand Down Expand Up @@ -253,17 +258,49 @@ sortedEntities.propTypes = {
source: sourcePropType,
}

export function SourceEntities({ metric, metric_uuid, reload, report, source }) {
export function SourceEntities({ loading, measurements, metric, metric_uuid, reload, report, source_uuid }) {
const dataModel = useContext(DataModel)
const [hideIgnoredEntities, setHideIgnoredEntities] = useState(false)
const [sortColumn, setSortColumn] = useState(null)
const [columnType, setColumnType] = useState("text")
const [sortDirection, setSortDirection] = useState("ascending")

const reportSource = metric.sources[source.source_uuid]
const metricEntities = dataModel.sources[reportSource.type].entities[metric.type]
if (!metricEntities || !Array.isArray(source.entities) || source.entities.length === 0) {
return null
const sourceType = metric.sources[source_uuid].type
const metricEntities = dataModel.sources[sourceType]?.entities?.[metric.type]

if (!metricEntities) {
const unit = dataModel.metrics[metric.type].unit
const sourceTypeName = dataModel.sources[sourceType].name
return (
<Message
header="Measurement details not available"
content={`Showing individual ${unit} is not supported when using ${sourceTypeName} as source.`}
/>
)
}
if (loading === "failed") {
return <FailedToLoadMeasurementsWarningMessage />
}
if (loading === "loading") {
return <LoadingPlaceHolder />
}
if (measurements.length === 0) {
return (
<Message
header="No measurements available"
content="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 (
<Message
header="Measurement details not available"
content="There are currently no measurement details available."
/>
)
}
const entityAttributes = metricEntities.attributes.filter((attribute) => attribute?.visible ?? true)
const sortProps = {
Expand Down Expand Up @@ -305,9 +342,11 @@ export function SourceEntities({ metric, metric_uuid, reload, report, source })
)
}
SourceEntities.propTypes = {
loading: loadingPropType,
measurements: measurementsPropType,
metric: metricPropType,
metric_uuid: string,
reload: func,
report: reportPropType,
source: sourcePropType,
source_uuid: string,
}
47 changes: 41 additions & 6 deletions components/frontend/src/source/SourceEntities.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ const dataModel = {
},
},
},
source_type_without_entities: {},
},
metrics: {
metric_type: {
unit: "items",
},
},
}

const metric = {
const metricFixture = {
type: "metric_type",
sources: {
source_uuid: {
Expand Down Expand Up @@ -93,19 +99,48 @@ function expectOrder(expected) {
}
}

function renderSourceEntities({ source = sourceFixture } = {}) {
render(
function renderSourceEntities({
loading = "loaded",
measurements = [{ sources: [sourceFixture] }],
metric = metricFixture,
} = {}) {
return render(
<DataModel.Provider value={dataModel}>
<SourceEntities metric={metric} report={{ issue_tracker: null }} source={source} />
<SourceEntities
loading={loading}
measurements={measurements}
metric={metric}
metric_uuid="metric_uuid"
report={{ issue_tracker: null }}
source_uuid="source_uuid"
/>
</DataModel.Provider>,
)
}

it("does not render when there are no source entities", async () => {
renderSourceEntities({ source: { source_uuid: "source_uuid", entities: [] } })
it("renders a message if the metric does not have measurement entities", () => {
renderSourceEntities({
metric: { type: "metric_type", sources: { source_uuid: { type: "source_type_without_entities" } } },
})
expect(screen.getAllByText(/Measurement details not available/).length).toBe(1)
})

it("renders a message if the measurements failed to load", () => {
renderSourceEntities({ loading: "failed" })
expect(screen.getAllByText(/Loading measurements failed/).length).toBe(1)
})

it("renders a placeholder while the measurements are loading", () => {
const { container } = renderSourceEntities({ loading: "loading" })
expect(container.firstChild.className).toContain("placeholder")
expect(screen.queryAllByText("AAA").length).toBe(0)
})

it("renders a message if there are no measurements", () => {
renderSourceEntities({ measurements: [] })
expect(screen.getAllByText(/No measurements/).length).toBe(1)
})

it("shows the hide resolved entities button", async () => {
renderSourceEntities()
const hideEntitiesButton = screen.getAllByRole("button")[0]
Expand Down
18 changes: 18 additions & 0 deletions components/frontend/src/widgets/Placeholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { number } from "prop-types"
import { useContext } from "react"
import { Placeholder, PlaceholderImage } from "semantic-ui-react"

import { DarkMode } from "../context/DarkMode"

export function LoadingPlaceHolder({ height }) {
const darkMode = useContext(DarkMode)
const defaultHeight = 400
return (
<Placeholder fluid inverted={darkMode} style={{ height: height ?? defaultHeight }}>
<PlaceholderImage />
</Placeholder>
)
}
LoadingPlaceHolder.propTypes = {
height: number,
}
9 changes: 9 additions & 0 deletions components/frontend/src/widgets/WarningMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ export function WarningMessage(props) {
WarningMessage.propTypes = {
showIf: bool,
}

export function FailedToLoadMeasurementsWarningMessage() {
return (
<WarningMessage
content="Loading the measurements from the API-server failed."
header="Loading measurements failed"
/>
)
}
1 change: 1 addition & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ If your currently installed *Quality-time* version is not v5.14.0, please first
### Changed

- Always show metric trend graph tabs. Display a loader in the tab while loading measurements. If the trend graph cannot be displayed, explain in the tab why not. Closes [#8825](https://github.com/ICTU/quality-time/issues/8825).
- Always show measurement entity tabs. Display a loader in the tab while loading measurements. If the measurement entities cannot be displayed, explain in the tab why not. Closes [#8826](https://github.com/ICTU/quality-time/issues/8826).

## v5.14.0 - 2024-07-05

Expand Down

0 comments on commit 3b8605d

Please sign in to comment.