ICTU/quality-time

View on GitHub
components/frontend/src/subject/SubjectTableRow.js

Summary

Maintainability
D
1 day
Test Coverage
import { bool, func, number, object, string } from "prop-types"
import { useContext } from "react"

import { DarkMode } from "../context/DarkMode"
import { DataModel } from "../context/DataModel"
import { IssueStatus } from "../issue/IssueStatus"
import { MeasurementSources } from "../measurement/MeasurementSources"
import { MeasurementTarget } from "../measurement/MeasurementTarget"
import { MeasurementValue } from "../measurement/MeasurementValue"
import { Overrun } from "../measurement/Overrun"
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 {
    dataModelPropType,
    datePropType,
    datesPropType,
    directionPropType,
    measurementsPropType,
    metricPropType,
    optionalDatePropType,
    reportPropType,
    reportsPropType,
    scalePropType,
    settingsPropType,
    stringsPropType,
} from "../sharedPropTypes"
import {
    formatMetricScale,
    formatMetricScaleAndUnit,
    formatMetricValue,
    getMetricDirection,
    getMetricName,
    getMetricScale,
    getMetricTags,
    getMetricUnit,
    limitTextLength,
} from "../utils"
import { TableRowWithDetails } from "../widgets/TableRowWithDetails"
import { Tag } from "../widgets/Tag"

function didValueIncrease(dateOrderAscending, metricValue, previousValue, scale) {
    let value = metricValue
    let previous = previousValue
    if (scale !== "version_number") {
        value = Number(metricValue)
        previous = Number(previousValue)
    }
    return (dateOrderAscending && value > previous) || (!dateOrderAscending && value < previous)
}
didValueIncrease.propTypes = {
    dateOrderAscending: bool,
    metricValue: string,
    previousValue: string,
    scale: scalePropType,
}

function didValueImprove(didValueIncrease, direction) {
    return (didValueIncrease && direction === ">") || (!didValueIncrease && direction === "<")
}
didValueImprove.propTypes = {
    didValueIncrease: bool,
    direction: directionPropType,
}

function deltaColor(metric, improved) {
    const evaluateTarget = metric.evaluate_targets ?? true
    if (evaluateTarget) {
        return improved ? "green" : "red"
    }
    return "blue"
}
deltaColor.propTypes = {
    metric: metricPropType,
    improved: bool,
}

function deltaDescription(dataModel, metric, scale, delta, improved, oldValue, newValue) {
    let description = `${getMetricName(metric, dataModel)} `
    const evaluateTarget = metric.evaluate_targets ?? true
    if (evaluateTarget) {
        description += improved ? "improved" : "worsened"
    } else {
        description += `changed`
    }
    description += ` from ${formatMetricValue(scale, oldValue)} to ${formatMetricValue(scale, newValue)}`
    if (scale !== "version_number") {
        const unit = formatMetricScaleAndUnit(metric, dataModel)
        description += `${unit} by ${delta}${unit}`
    }
    return description
}
deltaDescription.propTypes = {
    dataModel: object,
    metric: metricPropType,
    scale: string,
    delta: string,
    improved: bool,
    oldValue: string,
    newValue: string,
}

function deltaLabel(increased, scale, metricValue, previousValue) {
    let delta = increased ? "+" : "-"
    if (scale !== "version_number") {
        delta += `${formatMetricValue(scale, Math.abs(metricValue - previousValue))}`
    }
    return delta
}
deltaLabel.propTypes = {
    increased: bool,
    scale: string,
    metricValue: string,
    previousValue: string,
}

function DeltaCell({ dateOrderAscending, index, metric, metricValue, previousValue, status }) {
    const dataModel = useContext(DataModel)
    let label = null
    if (index > 0 && previousValue !== "?" && metricValue !== "?" && previousValue !== metricValue) {
        // Note that the delta cell only gets content if the previous and current values are both available and unequal
        const scale = getMetricScale(metric, dataModel)
        const increased = didValueIncrease(dateOrderAscending, metricValue, previousValue, scale)
        const delta = deltaLabel(increased, scale, metricValue, previousValue)
        const oldValue = dateOrderAscending ? previousValue : metricValue
        const newValue = dateOrderAscending ? metricValue : previousValue
        const direction = getMetricDirection(metric, dataModel)
        const improved = didValueImprove(increased, direction)
        const description = deltaDescription(dataModel, metric, scale, delta, improved, oldValue, newValue)
        const color = deltaColor(metric, improved)
        label = (
            <Popup
                content={description}
                trigger={
                    <Label aria-label={description} basic color={color}>
                        {delta}
                    </Label>
                }
            />
        )
    }
    return (
        <Table.Cell className={status} singleLine textAlign="right">
            {label}
        </Table.Cell>
    )
}
DeltaCell.propTypes = {
    dateOrderAscending: bool,
    metric: metricPropType,
    index: number,
    metricValue: string,
    previousValue: string,
    status: string,
}

function measurementOnDate(metric_uuid, measurements, date) {
    const isoDateString = date.toISOString().split("T")[0]
    return measurements?.find((m) => {
        return (
            m.metric_uuid === metric_uuid &&
            m.start.split("T")[0] <= isoDateString &&
            isoDateString <= m.end.split("T")[0]
        )
    })
}
measurementOnDate.propTypes = {
    metric_uuid: string,
    measurements: measurementsPropType,
    date: datePropType,
}

function metricValueAndStatusOnDate(dataModel, metric, metric_uuid, measurements, date) {
    const measurement = measurementOnDate(metric_uuid, measurements, date)
    const scale = getMetricScale(metric, dataModel)
    return [measurement?.[scale]?.value ?? "?", measurement?.[scale]?.status ?? "unknown"]
}
metricValueAndStatusOnDate.propTypes = {
    dataModel: dataModelPropType,
    metric: metricPropType,
    metric_uuid: string,
    measurements: measurementsPropType,
    date: datePropType,
}

function MeasurementCells({ dates, metric, metric_uuid, measurements, settings }) {
    const dataModel = useContext(DataModel)
    const showDeltaColumns = settings.hiddenColumns.excludes("delta")
    const dateOrderAscending = settings.dateOrder.value === "ascending"
    const scale = getMetricScale(metric, dataModel)
    const cells = []
    let previousValue = "?"
    dates.forEach((date, index) => {
        const [metricValue, status] = metricValueAndStatusOnDate(dataModel, metric, metric_uuid, measurements, date)
        if (showDeltaColumns && index > 0) {
            cells.push(
                <DeltaCell
                    dateOrderAscending={dateOrderAscending}
                    index={index}
                    key={`${date}-delta`}
                    metric={metric}
                    metricValue={metricValue}
                    previousValue={previousValue}
                    status={status}
                />,
            )
        }
        cells.push(
            <Table.Cell className={status} key={date} textAlign="right">
                {formatMetricValue(scale, metricValue)}
                {formatMetricScale(metric, dataModel)}
            </Table.Cell>,
        )
        previousValue = metricValue === "?" ? previousValue : metricValue
    })
    return cells
}
MeasurementCells.propTypes = {
    dates: datesPropType,
    measurements: measurementsPropType,
    metric_uuid: string,
    metric: metricPropType,
    settings: settingsPropType,
}

function expandOrCollapseItem(expand, metric_uuid, expandedItems) {
    if (expand) {
        expandedItems.toggle(`${metric_uuid}:0`)
    } else {
        const items = expandedItems.value.filter((each) => each?.startsWith(metric_uuid))
        expandedItems.toggle(items[0])
    }
}

export function SubjectTableRow({
    changed_fields,
    dates,
    handleSort,
    index,
    lastIndex,
    measurements,
    metric_uuid,
    metric,
    reload,
    report,
    reportDate,
    reports,
    reversedMeasurements,
    settings,
    subject_uuid,
}) {
    const dataModel = useContext(DataModel)
    const darkMode = useContext(DarkMode)
    const metricName = getMetricName(metric, dataModel)
    const scale = getMetricScale(metric, dataModel)
    const unit = getMetricUnit(metric, dataModel)
    const nrDates = dates.length
    const style = nrDates > 1 ? { background: darkMode ? "rgba(60, 60, 60, 1)" : "#f9fafb" } : {}
    return (
        <TableRowWithDetails
            className={nrDates === 1 ? metric.status || "unknown" : ""}
            details={
                <MetricDetails
                    changed_fields={changed_fields}
                    first_metric={index === 0}
                    last_metric={index === lastIndex}
                    metric_uuid={metric_uuid}
                    reload={reload}
                    report_date={reportDate}
                    reports={reports}
                    report={report}
                    stopFilteringAndSorting={() => {
                        handleSort(null)
                        settings.hiddenTags.reset()
                        settings.metricsToHide.reset()
                    }}
                    subject_uuid={subject_uuid}
                    expandedItems={settings.expandedItems}
                />
            }
            expanded={settings.expandedItems.value.filter((item) => item?.startsWith(metric_uuid)).length > 0}
            id={metric_uuid}
            onExpand={(expand) => expandOrCollapseItem(expand, metric_uuid, settings.expandedItems)}
            style={style}
        >
            <Table.Cell style={style}>{metricName}</Table.Cell>
            {nrDates > 1 && (
                <MeasurementCells
                    dates={dates}
                    metric={metric}
                    metric_uuid={metric_uuid}
                    measurements={reversedMeasurements}
                    settings={settings}
                />
            )}
            {nrDates === 1 && settings.hiddenColumns.excludes("trend") && (
                <Table.Cell>
                    <TrendSparkline measurements={metric.recent_measurements} report_date={reportDate} scale={scale} />
                </Table.Cell>
            )}
            {nrDates === 1 && settings.hiddenColumns.excludes("status") && (
                <Table.Cell textAlign="center">
                    <StatusIcon status={metric.status} statusStart={metric.status_start} />
                </Table.Cell>
            )}
            {nrDates === 1 && settings.hiddenColumns.excludes("measurement") && (
                <Table.Cell textAlign="right">
                    <MeasurementValue metric={metric} reportDate={reportDate} />
                </Table.Cell>
            )}
            {nrDates === 1 && settings.hiddenColumns.excludes("target") && (
                <Table.Cell textAlign="right">
                    <MeasurementTarget metric={metric} />
                </Table.Cell>
            )}
            {settings.hiddenColumns.excludes("unit") && <Table.Cell style={style}>{unit}</Table.Cell>}
            {settings.hiddenColumns.excludes("source") && (
                <Table.Cell style={style}>
                    <MeasurementSources metric={metric} />
                </Table.Cell>
            )}
            {settings.hiddenColumns.excludes("time_left") && (
                <Table.Cell style={style}>
                    <TimeLeft metric={metric} report={report} />
                </Table.Cell>
            )}
            {nrDates > 1 && settings.hiddenColumns.excludes("overrun") && (
                <Table.Cell style={style}>
                    <Overrun
                        metric={metric}
                        metric_uuid={metric_uuid}
                        report={report}
                        measurements={measurements}
                        dates={dates}
                    />
                </Table.Cell>
            )}
            {settings.hiddenColumns.excludes("comment") && (
                <Table.Cell style={style}>
                    <div
                        style={{ wordBreak: "break-word" }}
                        dangerouslySetInnerHTML={{ __html: limitTextLength(metric.comment) }}
                    />
                </Table.Cell>
            )}
            {settings.hiddenColumns.excludes("issues") && (
                <Table.Cell style={style}>
                    <IssueStatus metric={metric} issueTrackerMissing={!report.issue_tracker} settings={settings} />
                </Table.Cell>
            )}
            {settings.hiddenColumns.excludes("tags") && (
                <Table.Cell style={style}>
                    {getMetricTags(metric).map((tag) => (
                        <Tag key={tag} tag={tag} />
                    ))}
                </Table.Cell>
            )}
        </TableRowWithDetails>
    )
}
SubjectTableRow.propTypes = {
    changed_fields: stringsPropType,
    dates: datesPropType,
    handleSort: func,
    index: number,
    lastIndex: number,
    measurements: measurementsPropType,
    metric_uuid: string,
    metric: metricPropType,
    reload: func,
    report: reportPropType,
    reportDate: optionalDatePropType,
    reports: reportsPropType,
    reversedMeasurements: measurementsPropType,
    settings: settingsPropType,
    subject_uuid: string,
}