ICTU/quality-time

View on GitHub
components/frontend/src/source/Source.js

Summary

Maintainability
C
1 day
Test Coverage
import { bool, func, object, oneOfType, string } from "prop-types"
import { useContext } from "react"
import { Grid, Menu } from "semantic-ui-react"

import { delete_source, set_source_attribute } from "../api/source"
import { ChangeLog } from "../changelog/ChangeLog"
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 {
    measurementSourcePropType,
    metricPropType,
    reportPropType,
    sourcePropType,
    stringsPropType,
} from "../sharedPropTypes"
import { getMetricName, getSourceName } from "../utils"
import { DeleteButton, ReorderButtonGroup } from "../widgets/Button"
import { FocusableTab } from "../widgets/FocusableTab"
import { HyperLink } from "../widgets/HyperLink"
import { SourceParameters } from "./SourceParameters"
import { SourceType } from "./SourceType"
import { SourceTypeHeader } from "./SourceTypeHeader"

function select_sources_parameter_keys(changed_fields, source_uuid) {
    return changed_fields
        ? changed_fields.filter((field) => field.source_uuid === source_uuid).map((field) => field.parameter_key)
        : []
}

function ButtonGridRow({ first_source, last_source, reload, source_uuid }) {
    return (
        <ReadOnlyOrEditable
            requiredPermissions={[EDIT_REPORT_PERMISSION]}
            editableComponent={
                <div style={{ marginTop: "20px" }}>
                    <Grid.Row>
                        <Grid.Column>
                            <ReorderButtonGroup
                                first={first_source}
                                last={last_source}
                                moveable="source"
                                onClick={(direction) => {
                                    set_source_attribute(source_uuid, "position", direction, reload)
                                }}
                            />
                            <DeleteButton itemType="source" onClick={() => delete_source(source_uuid, reload)} />
                        </Grid.Column>
                    </Grid.Row>
                </div>
            }
        />
    )
}
ButtonGridRow.propTypes = {
    first_source: bool,
    last_source: bool,
    reload: func,
    source_uuid: string,
}

function Parameters({
    changed_fields,
    config_error,
    connection_error,
    metric,
    parse_error,
    reload,
    report,
    source,
    source_uuid,
}) {
    const dataModel = useContext(DataModel)
    const source_type = dataModel.sources[source.type]
    return (
        <Grid stackable>
            <Grid.Row columns={2}>
                <Grid.Column>
                    <SourceType
                        metric_type={metric.type}
                        set_source_attribute={(a, v) => set_source_attribute(source_uuid, a, v, reload)}
                        source_uuid={source_uuid}
                        source_type={source.type}
                    />
                </Grid.Column>
                <Grid.Column>
                    <StringInput
                        requiredPermissions={[EDIT_REPORT_PERMISSION]}
                        id="source-name"
                        label="Source name"
                        placeholder={source_type.name}
                        set_value={(value) => set_source_attribute(source_uuid, "name", value, reload)}
                        value={source.name}
                    />
                </Grid.Column>
            </Grid.Row>
            <Grid.Row columns={1}>
                <Grid.Column>
                    <SourceParameters
                        changed_param_keys={select_sources_parameter_keys(changed_fields, source_uuid)}
                        metric={metric}
                        reload={reload}
                        report={report}
                        source={source}
                        source_uuid={source_uuid}
                    />
                </Grid.Column>
            </Grid.Row>
            {connection_error && <ErrorMessage title="Connection error" message={connection_error} />}
            {parse_error && <ErrorMessage title="Parse error" message={parse_error} />}
            {config_error && <ErrorMessage title="Configuration error" message={config_error} formatAsText={true} />}
        </Grid>
    )
}
Parameters.propTypes = {
    changed_fields: stringsPropType,
    config_error: oneOfType([object, string]),
    connection_error: string,
    metric: metricPropType,
    parse_error: string,
    reload: func,
    report: reportPropType,
    source: sourcePropType,
    source_uuid: string,
}

export function Source({
    changed_fields,
    first_source,
    last_source,
    measurement_source,
    metric,
    reload,
    report,
    source_uuid,
}) {
    const dataModel = useContext(DataModel)
    const source = metric.sources[source_uuid]
    const sourceType = dataModel.sources[source.type]
    const sourceName = getSourceName(source, dataModel)
    const metricName = getMetricName(metric, dataModel)
    const connectionError = measurement_source?.connection_error || ""
    const parseError = measurement_source?.parse_error || ""
    const referenceManualURL = `https://quality-time.readthedocs.io/en/v${process.env.REACT_APP_VERSION}/reference.html`
    const configErrorMessage = (
        <>
            <p>
                {sourceName} cannot be used to measure {metricName}. This configuration error occurs if the type of a
                metric is changed to a metric type that is not supported by the configured source type(s).
            </p>
            <p>There are several ways to fix this:</p>
            <ul>
                <li>
                    Change the type of the metric (back) to a type that is supported by{" "}
                    <HyperLink url={`${referenceManualURL}#${source.type}`}>{sourceName}</HyperLink>.
                </li>
                <li>
                    Change the type of this source to a type that supports{" "}
                    <HyperLink url={`${referenceManualURL}#${metric.type}`}>{metricName}</HyperLink>.
                </li>
                <li>Move this source to another metric.</li>
                <li>Remove this source altogether.</li>
            </ul>
            <p>
                As {sourceName} cannot be used to measure {metricName}, no source parameters are currently visible. Any
                source parameters configured previously will become visible again when the metric type is changed back
                to the previous metric type.
            </p>
        </>
    )
    const configError = dataModel.metrics[metric.type].sources.includes(source.type) ? "" : configErrorMessage
    const configurationTabLabel =
        configError || connectionError || parseError ? <Label color="red">{"Configuration"}</Label> : "Configuration"
    const panes = [
        {
            menuItem: (
                <Menu.Item key="configuration">
                    <Icon name="settings" />
                    <FocusableTab>{configurationTabLabel}</FocusableTab>
                </Menu.Item>
            ),
            render: () => (
                <Tab.Pane>
                    <Parameters
                        metric={metric}
                        source={source}
                        source_uuid={source_uuid}
                        connection_error={connectionError}
                        parse_error={parseError}
                        config_error={configError}
                        report={report}
                        changed_fields={changed_fields}
                        reload={reload}
                    />
                </Tab.Pane>
            ),
        },
        {
            menuItem: (
                <Menu.Item key="changelog">
                    <Icon name="history" />
                    <FocusableTab>{"Changelog"}</FocusableTab>
                </Menu.Item>
            ),
            render: () => (
                <Tab.Pane>
                    <ChangeLog source_uuid={source_uuid} timestamp={report.timestamp} />
                </Tab.Pane>
            ),
        },
    ]
    return (
        <>
            <SourceTypeHeader metricTypeId={metric.type} sourceTypeId={source.type} sourceType={sourceType} />
            <Tab panes={panes} />
            <ButtonGridRow
                first_source={first_source}
                last_source={last_source}
                reload={reload}
                source_uuid={source_uuid}
            />
        </>
    )
}
Source.propTypes = {
    changed_fields: stringsPropType,
    first_source: bool,
    last_source: bool,
    measurement_source: measurementSourcePropType,
    metric: metricPropType,
    reload: func,
    report: reportPropType,
    source_uuid: string,
}