• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CBIIT / crdc-datahub-ui / 18789341118

24 Oct 2025 06:57PM UTC coverage: 78.178% (+15.5%) from 62.703%
18789341118

push

github

web-flow
Merge pull request #888 from CBIIT/3.4.0

3.4.0 Release

4977 of 5488 branches covered (90.69%)

Branch coverage included in aggregate %.

8210 of 9264 new or added lines in 257 files covered. (88.62%)

6307 existing lines in 120 files now uncovered.

30203 of 39512 relevant lines covered (76.44%)

213.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.47
/src/content/dataSubmissions/QualityControl.tsx
1
import { useLazyQuery, useQuery } from "@apollo/client";
1✔
2
import { Box, Button, Stack, styled, TableCell } from "@mui/material";
1✔
3
import { isEqual } from "lodash";
1✔
4
import { useSnackbar } from "notistack";
1✔
5
import React, {
1✔
6
  FC,
7
  MutableRefObject,
8
  useCallback,
9
  useEffect,
10
  useMemo,
11
  useRef,
12
  useState,
13
} from "react";
14

15
import { TOOLTIP_TEXT } from "@/config/DashboardTooltips";
1✔
16

17
import { useSubmissionContext } from "../../components/Contexts/SubmissionContext";
1✔
18
import { ExportValidationButton } from "../../components/DataSubmissions/ExportValidationButton";
1✔
19
import QualityControlFilters from "../../components/DataSubmissions/QualityControlFilters";
1✔
20
import DoubleLabelSwitch from "../../components/DoubleLabelSwitch";
1✔
21
import ErrorDetailsDialog, { ErrorDetailsIssue } from "../../components/ErrorDetailsDialog/v2";
1✔
22
import GenericTable, { Column } from "../../components/GenericTable";
1✔
23
import NodeComparison from "../../components/NodeComparison";
1✔
24
import PVRequestButton from "../../components/PVRequestButton";
1✔
25
import StyledTooltip from "../../components/StyledFormComponents/StyledTooltip";
1✔
26
import TruncatedText from "../../components/TruncatedText";
1✔
27
import { ValidationErrorCodes } from "../../config/ValidationErrors";
1✔
28
import {
1✔
29
  AGGREGATED_SUBMISSION_QC_RESULTS,
30
  SUBMISSION_QC_RESULTS,
31
  AggregatedSubmissionQCResultsInput,
32
  AggregatedSubmissionQCResultsResp,
33
  SubmissionQCResultsInput,
34
  SubmissionQCResultsResp,
35
  GET_PENDING_PVS,
36
  GetPendingPVsInput,
37
  GetPendingPVsResponse,
38
} from "../../graphql";
39
import { FormatDate, Logger, titleCase } from "../../utils";
1✔
40

41
import QCResultsContext from "./Contexts/QCResultsContext";
1✔
42

43
type FilterForm = {
44
  issueType: string;
45
  /**
46
   * The node type to filter by.
47
   *
48
   * @default "All"
49
   */
50
  nodeType: string;
51
  batchID: number | "All";
52
  severity: QCResult["severity"] | "All";
53
};
54

55
const StyledErrorDetailsButton = styled(Button)({
1✔
56
  display: "inline",
1✔
57
  color: "#0B6CB1",
1✔
58
  fontFamily: "'Nunito', 'Rubik', sans-serif",
1✔
59
  fontSize: "16px",
1✔
60
  fontStyle: "normal",
1✔
61
  fontWeight: 600,
1✔
62
  lineHeight: "19px",
1✔
63
  padding: 0,
1✔
64
  textDecorationLine: "underline",
1✔
65
  textTransform: "none",
1✔
66
  marginLeft: "auto",
1✔
67
  paddingLeft: "8px",
1✔
68
  "&:hover": {
1✔
69
    background: "transparent",
1✔
70
    textDecorationLine: "underline",
1✔
71
  },
1✔
72
});
1✔
73

74
const StyledNodeType = styled(Box)({
1✔
75
  display: "flex",
1✔
76
  alignItems: "center",
1✔
77
  textTransform: "capitalize",
1✔
78
});
1✔
79

80
const StyledSeverity = styled(Box)({
1✔
81
  display: "flex",
1✔
82
  alignItems: "center",
1✔
83
});
1✔
84

85
const StyledBreakAll = styled(Box)({
1✔
86
  wordBreak: "break-all",
1✔
87
});
1✔
88

89
const StyledIssuesTextWrapper = styled(Box)({
1✔
90
  whiteSpace: "nowrap",
1✔
91
  wordBreak: "break-word",
1✔
92
});
1✔
93

94
const StyledDateTooltip = styled(StyledTooltip)(() => ({
1✔
95
  cursor: "pointer",
8✔
96
}));
1✔
97

98
const StyledPvButtonWrapper = styled(Box)({
1✔
99
  marginLeft: "89px",
1✔
100
});
1✔
101

102
const StyledOthersText = styled("span")({
1✔
103
  display: "inline",
1✔
104
  textDecoration: "underline",
1✔
105
  cursor: "pointer",
1✔
106
  color: "#0B6CB1",
1✔
107
  whiteSpace: "nowrap",
1✔
108
  fontSize: "14px",
1✔
109
  fontStyle: "normal",
1✔
110
  fontWeight: 600,
1✔
111
  lineHeight: "19px",
1✔
112
});
1✔
113

114
const StyledHeaderCell = styled(TableCell)({
1✔
115
  fontWeight: 700,
1✔
116
  fontSize: "14px",
1✔
117
  lineHeight: "16px",
1✔
118
  color: "#fff !important",
1✔
119
  padding: "22px 4px",
1✔
120
  verticalAlign: "top",
1✔
121
  "&.MuiTableCell-root:first-of-type": {
1✔
122
    paddingTop: "22px",
1✔
123
    paddingRight: "4px",
1✔
124
    paddingBottom: "22px",
1✔
125
    color: "#fff !important",
1✔
126
  },
1✔
127
  "& .MuiSvgIcon-root, & .MuiButtonBase-root": {
1✔
128
    color: "#fff !important",
1✔
129
  },
1✔
130
});
1✔
131

132
const StyledTableCell = styled(TableCell)({
1✔
133
  fontSize: "14px",
1✔
134
  color: "#083A50 !important",
1✔
135
  "&.MuiTableCell-root": {
1✔
136
    padding: "14px 4px 12px",
1✔
137
    overflowWrap: "anywhere",
1✔
138
    whiteSpace: "nowrap",
1✔
139
  },
1✔
140
  "&:last-of-type": {
1✔
141
    paddingRight: "4px",
1✔
142
  },
1✔
143
});
1✔
144

145
type RowData = QCResult | AggregatedQCResult;
146

147
const aggregatedColumns: Column<AggregatedQCResult>[] = [
1✔
148
  {
1✔
149
    label: "Issue Type",
1✔
150
    renderValue: (data) => <TruncatedText text={data.title} maxCharacters={50} />,
1✔
151
    field: "title",
1✔
152
  },
1✔
153
  {
1✔
154
    label: "Severity",
1✔
155
    renderValue: (data) => (
1✔
156
      <StyledSeverity color={data?.severity === "Error" ? "#B54717" : "#8D5809"}>
2✔
157
        {data?.severity}
2✔
158
      </StyledSeverity>
2✔
159
    ),
160
    field: "severity",
1✔
161
  },
1✔
162
  {
1✔
163
    label: "Record Count",
1✔
164
    renderValue: (data) =>
1✔
165
      Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(data.count || 0),
2!
166
    field: "count",
1✔
167
    default: true,
1✔
168
  },
1✔
169
  {
1✔
170
    label: "Expand",
1✔
171
    renderValue: (data) => (
1✔
172
      <QCResultsContext.Consumer>
2✔
173
        {({ handleExpandClick }) => (
2✔
174
          <StyledErrorDetailsButton
2✔
175
            onClick={() => handleExpandClick?.(data)}
2✔
176
            variant="text"
2✔
177
            disableRipple
2✔
178
            disableTouchRipple
2✔
179
            disableFocusRipple
2✔
180
          >
181
            Expand
182
          </StyledErrorDetailsButton>
2✔
183
        )}
184
      </QCResultsContext.Consumer>
2✔
185
    ),
186
    fieldKey: "expand",
1✔
187
    sortDisabled: true,
1✔
188
    sx: {
1✔
189
      width: "104px",
1✔
190
      textAlign: "center",
1✔
191
    },
1✔
192
  },
1✔
193
];
194

195
const expandedColumns: Column<QCResult>[] = [
1✔
196
  {
1✔
197
    label: "Batch ID",
1✔
198
    renderValue: (data) => <StyledBreakAll>{data?.displayID}</StyledBreakAll>,
1✔
199
    field: "displayID",
1✔
200
    default: true,
1✔
201
    sx: {
1✔
202
      width: "110px",
1✔
203
    },
1✔
204
  },
1✔
205
  {
1✔
206
    label: "Node Type",
1✔
207
    renderValue: (data) => (
1✔
208
      <StyledNodeType>
18✔
209
        <TruncatedText text={data?.type} maxCharacters={12} disableInteractiveTooltip={false} />
18✔
210
      </StyledNodeType>
18✔
211
    ),
212
    field: "type",
1✔
213
  },
1✔
214
  {
1✔
215
    label: "Submitted Identifier",
1✔
216
    renderValue: (data) => (
1✔
217
      <TruncatedText
18✔
218
        text={data?.submittedID}
18✔
219
        maxCharacters={15}
18✔
220
        disableInteractiveTooltip={false}
18✔
221
      />
222
    ),
223
    field: "submittedID",
1✔
224
  },
1✔
225
  {
1✔
226
    label: "Severity",
1✔
227
    renderValue: (data) => (
1✔
228
      <StyledSeverity color={data?.severity === "Error" ? "#B54717" : "#8D5809"}>
18✔
229
        {data?.severity}
18✔
230
      </StyledSeverity>
18✔
231
    ),
232
    field: "severity",
1✔
233
    sx: {
1✔
234
      width: "87px",
1✔
235
    },
1✔
236
  },
1✔
237
  {
1✔
238
    label: "Validated Date",
1✔
239
    renderValue: (data) =>
1✔
240
      data.validatedDate ? (
18✔
241
        <StyledDateTooltip
10✔
242
          title={FormatDate(data.validatedDate, "M/D/YYYY h:mm A")}
10✔
243
          placement="top"
10✔
244
        >
245
          <span>{FormatDate(data.validatedDate, "M/D/YYYY")}</span>
10✔
246
        </StyledDateTooltip>
10✔
247
      ) : (
248
        ""
8✔
249
      ),
250
    field: "validatedDate",
1✔
251
    sx: {
1✔
252
      width: "132px",
1✔
253
    },
1✔
254
  },
1✔
255
  {
1✔
256
    label: "Issue Count",
1✔
257
    renderValue: (data) =>
1✔
258
      Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(data.issueCount || 0),
18✔
259
    field: "issueCount",
1✔
260
    sx: {
1✔
261
      width: "110px",
1✔
262
    },
1✔
263
  },
1✔
264
  {
1✔
265
    label: "Issue(s)",
1✔
266
    renderValue: (data) =>
1✔
267
      (data?.errors?.length > 0 || data?.warnings?.length > 0) && (
18✔
268
        <QCResultsContext.Consumer>
10✔
269
          {({ handleOpenErrorDialog }) => (
10✔
270
            <Stack direction="row" justifyContent="space-between">
8✔
271
              <StyledIssuesTextWrapper>
8✔
272
                <TruncatedText
8✔
273
                  text={data.errors?.[0]?.title || data.warnings?.[0]?.title}
8✔
274
                  maxCharacters={30}
8✔
275
                  wrapperSx={{ display: "inline" }}
8✔
276
                  labelSx={{ display: "inline" }}
8✔
277
                  disableInteractiveTooltip={false}
8✔
278
                />
279
                {data.issueCount > 1 ? (
8✔
280
                  <>
6✔
281
                    {" and "}
6✔
282
                    <StyledTooltip
6✔
283
                      title={TOOLTIP_TEXT.QUALITY_CONTROL.TABLE.CLICK_TO_VIEW_ALL_ISSUES}
6✔
284
                      placement="top"
6✔
285
                      disableInteractive
6✔
286
                      arrow
6✔
287
                    >
288
                      <StyledOthersText
6✔
289
                        onClick={() => handleOpenErrorDialog?.(data)}
6✔
290
                        data-testid="others-text"
6✔
291
                      >
292
                        other {data.issueCount - 1}
6✔
293
                      </StyledOthersText>
6✔
294
                    </StyledTooltip>
6✔
295
                  </>
6✔
296
                ) : null}
2✔
297
              </StyledIssuesTextWrapper>
8✔
298

299
              <StyledErrorDetailsButton
8✔
300
                onClick={() => handleOpenErrorDialog?.(data)}
8✔
301
                variant="text"
8✔
302
                disableRipple
8✔
303
                disableTouchRipple
8✔
304
                disableFocusRipple
8✔
305
              >
306
                See details.
307
              </StyledErrorDetailsButton>
8✔
308
            </Stack>
8✔
309
          )}
310
        </QCResultsContext.Consumer>
10✔
311
      ),
312
    sortDisabled: true,
1✔
313
  },
1✔
314
];
315

316
// CSV columns used for exporting table data
317
export const csvColumns = {
1✔
318
  "Batch ID": (d: QCResult) => d.displayID,
1✔
319
  "Node Type": (d: QCResult) => d.type,
1✔
320
  "Submitted Identifier": (d: QCResult) => d.submittedID,
1✔
321
  Severity: (d: QCResult) => d.severity,
1✔
322
  "Validated Date": (d: QCResult) => FormatDate(d?.validatedDate, "MM-DD-YYYY [at] hh:mm A", ""),
1✔
323
  Issues: (d: QCResult) => {
1✔
324
    const value = d.errors[0]?.description ?? d.warnings[0]?.description;
×
325

326
    // NOTE: The ErrorMessage descriptions contain non-standard double quotes
327
    // that don't render correctly in Excel. This replaces them with standard double quotes.
328
    return value.replaceAll(/[“”‟〞"]/g, `"`);
×
UNCOV
329
  },
×
330
};
1✔
331

332
export const aggregatedCSVColumns = {
1✔
333
  "Issue Type": (d: AggregatedQCResult) => d.title,
1✔
334
  Severity: (d: AggregatedQCResult) => d.severity,
1✔
335
  Count: (d: AggregatedQCResult) => d.count,
1✔
336
};
1✔
337

338
const QualityControl: FC = () => {
1✔
339
  const { enqueueSnackbar } = useSnackbar();
183✔
340
  const { data: submissionData } = useSubmissionContext();
183✔
341
  const {
183✔
342
    _id: submissionId,
183✔
343
    status: submissionStatus,
183✔
344
    metadataValidationStatus,
183✔
345
    fileValidationStatus,
183✔
346
  } = submissionData?.getSubmission || {};
183!
347

348
  const [loading, setLoading] = useState<boolean>(false);
183✔
349
  const [data, setData] = useState<RowData[]>([]);
183✔
350
  const [prevData, setPrevData] = useState<FetchListing<RowData>>(null);
183✔
351
  const [totalData, setTotalData] = useState(0);
183✔
352
  const [openErrorDialog, setOpenErrorDialog] = useState<boolean>(false);
183✔
353
  const [selectedRow, setSelectedRow] = useState<RowData | null>(null);
183✔
354
  const [isAggregated, setIsAggregated] = useState<boolean>(true);
183✔
355
  const [issueType, setIssueType] = useState<string | null>("All");
183✔
356
  const filtersRef: MutableRefObject<FilterForm> = useRef({
183✔
357
    issueType: "All",
183✔
358
    batchID: "All",
183✔
359
    nodeType: "All",
183✔
360
    severity: "All",
183✔
361
  });
183✔
362
  const tableRef = useRef<TableMethods>(null);
183✔
363

364
  const [submissionQCResults] = useLazyQuery<SubmissionQCResultsResp, SubmissionQCResultsInput>(
183✔
365
    SUBMISSION_QC_RESULTS,
183✔
366
    {
183✔
367
      context: { clientName: "backend" },
183✔
368
      fetchPolicy: "cache-and-network",
183✔
369
    }
183✔
370
  );
183✔
371

372
  const [aggregatedSubmissionQCResults] = useLazyQuery<
183✔
373
    AggregatedSubmissionQCResultsResp,
374
    AggregatedSubmissionQCResultsInput
375
  >(AGGREGATED_SUBMISSION_QC_RESULTS, {
183✔
376
    context: { clientName: "backend" },
183✔
377
    fetchPolicy: "cache-and-network",
183✔
378
  });
183✔
379

380
  const {
183✔
381
    data: pendingPVs,
183✔
382
    refetch: refetchPendingPVs,
183✔
383
    updateQuery: updatePendingPVs,
183✔
384
  } = useQuery<GetPendingPVsResponse, GetPendingPVsInput>(GET_PENDING_PVS, {
183✔
385
    variables: { submissionID: submissionId },
183✔
386
    context: { clientName: "backend" },
183✔
387
    fetchPolicy: "cache-and-network",
183✔
388
    notifyOnNetworkStatusChange: true,
183✔
389
    skip: !submissionId,
183✔
390
  });
183✔
391

392
  const handleFetchQCResults = async (fetchListing: FetchListing<QCResult>, force: boolean) => {
183✔
393
    const { first, offset, sortDirection, orderBy } = fetchListing || {};
23!
394
    if (!force && data?.length > 0 && isEqual(fetchListing, prevData)) {
23!
395
      return;
×
UNCOV
396
    }
×
397

398
    setPrevData(fetchListing);
23✔
399

400
    try {
23✔
401
      setLoading(true);
23✔
402

403
      const { data: d, error } = await submissionQCResults({
23✔
404
        variables: {
23✔
405
          id: submissionId,
23✔
406
          first,
23✔
407
          offset,
23✔
408
          sortDirection,
23✔
409
          orderBy,
23✔
410
          issueCode:
23✔
411
            !filtersRef.current.issueType || filtersRef.current.issueType === "All"
23✔
412
              ? undefined
23!
UNCOV
413
              : filtersRef.current.issueType,
×
414
          nodeTypes:
23✔
415
            !filtersRef.current.nodeType || filtersRef.current.nodeType === "All"
23✔
416
              ? undefined
22✔
417
              : [filtersRef.current.nodeType],
1✔
418
          batchIDs:
23✔
419
            !filtersRef.current.batchID || filtersRef.current.batchID === "All"
23✔
420
              ? undefined
21✔
421
              : [filtersRef.current.batchID],
2✔
422
          severities: filtersRef.current.severity || "All",
23!
423
        },
23✔
424
        context: { clientName: "backend" },
23✔
425
        fetchPolicy: "no-cache",
23✔
426
      });
23✔
427
      if (error || !d?.submissionQCResults) {
23✔
428
        throw new Error("Unable to retrieve submission quality control results.");
2✔
429
      }
2✔
430
      setData(d.submissionQCResults.results);
21✔
431
      setTotalData(d.submissionQCResults.total);
21✔
432
    } catch (err) {
23✔
433
      enqueueSnackbar(err?.toString(), { variant: "error" });
2✔
434
    } finally {
23✔
435
      setLoading(false);
23✔
436
    }
23✔
437
  };
23✔
438

439
  const handleFetchAggQCResults = async (
183✔
440
    fetchListing: FetchListing<AggregatedQCResult>,
38✔
441
    force: boolean
38✔
442
  ) => {
38✔
443
    const { first, offset, sortDirection, orderBy } = fetchListing || {};
38!
444

445
    if (!force && data?.length > 0 && isEqual(fetchListing, prevData)) {
38!
446
      return;
×
UNCOV
447
    }
×
448

449
    setPrevData(fetchListing);
38✔
450

451
    try {
38✔
452
      setLoading(true);
38✔
453

454
      const { data: d, error } = await aggregatedSubmissionQCResults({
38✔
455
        variables: {
38✔
456
          submissionID: submissionId,
38✔
457
          severity: filtersRef.current.severity?.toLowerCase() || "all",
38!
458
          first,
38✔
459
          offset,
38✔
460
          sortDirection,
38✔
461
          orderBy,
38✔
462
        },
38✔
463
        context: { clientName: "backend" },
38✔
464
        fetchPolicy: "no-cache",
38✔
465
      });
38✔
466
      if (error || !d?.aggregatedSubmissionQCResults) {
38✔
467
        throw new Error("Unable to retrieve submission aggregated quality control results.");
4✔
468
      }
4✔
469
      setData(d.aggregatedSubmissionQCResults.results);
34✔
470
      setTotalData(d.aggregatedSubmissionQCResults.total);
34✔
471
    } catch (err) {
38✔
472
      Logger.error(`QualityControl: ${err?.toString()}`);
4✔
473
      enqueueSnackbar(err?.toString(), { variant: "error" });
4✔
474
    } finally {
38✔
475
      setLoading(false);
38✔
476
    }
38✔
477
  };
38✔
478

479
  const handleFetchData = (fetchListing: FetchListing<RowData>, force: boolean) => {
183✔
480
    if (!submissionId || !filtersRef.current) {
63✔
481
      return;
2✔
482
    }
2✔
483

484
    isAggregated
61✔
485
      ? handleFetchAggQCResults(fetchListing, force)
38✔
486
      : handleFetchQCResults(fetchListing, force);
23✔
487
  };
63✔
488

489
  const handleOpenErrorDialog = (data: QCResult) => {
183✔
490
    setOpenErrorDialog(true);
1✔
491
    setSelectedRow(data);
1✔
492
  };
1✔
493

494
  const Actions = useMemo<React.ReactNode>(
183✔
495
    () => (
183✔
496
      <Stack direction="row" alignItems="center" gap="8px" marginRight="37px">
56✔
497
        <ExportValidationButton
56✔
498
          submission={submissionData?.getSubmission}
56✔
499
          fields={isAggregated ? aggregatedCSVColumns : csvColumns}
56✔
500
          isAggregated={isAggregated}
56✔
501
          disabled={totalData <= 0}
56✔
502
        />
503
      </Stack>
56✔
504
    ),
505
    [submissionData?.getSubmission, totalData, isAggregated]
183✔
506
  );
183✔
507

508
  const handleOnFiltersChange = (data: FilterForm) => {
183✔
509
    filtersRef.current = data;
36✔
510
    tableRef.current?.setPage(0, true);
36✔
511
  };
36✔
512

513
  const onSwitchToggle = () => {
183✔
514
    setIsAggregated((prev) => {
14✔
515
      const newVal = !prev;
14✔
516
      // Reset to 'All' when in Aggregated view
517
      if (newVal === true) {
14!
518
        setIssueType("All");
×
UNCOV
519
      }
×
520

521
      return newVal;
14✔
522
    });
14✔
523
  };
14✔
524

525
  const currentColumns = useMemo(
183✔
526
    () => (isAggregated ? aggregatedColumns : expandedColumns),
183✔
527
    [isAggregated]
183✔
528
  ) as Column<RowData>[];
183✔
529

530
  const handleExpandClick = (issue: AggregatedQCResult) => {
183✔
531
    if (!issue?.code) {
×
532
      Logger.error("QualityControl: Unable to expand invalid issue.");
×
533
      return;
×
UNCOV
534
    }
×
535

536
    setIssueType(issue?.code);
×
537
    setIsAggregated(false);
×
UNCOV
538
  };
×
539

540
  const providerValue = useMemo(
183✔
541
    () => ({
183✔
542
      handleOpenErrorDialog,
183✔
543
      handleExpandClick,
183✔
544
    }),
183✔
545
    [handleOpenErrorDialog, handleExpandClick]
183✔
546
  );
183✔
547

548
  const handleNewPVRequest = useCallback(
183✔
549
    (offendingProperty: string, offendingValue: string) => {
183✔
NEW
550
      updatePendingPVs((prev) => ({
×
NEW
551
        getPendingPVs: [
×
NEW
552
          ...(prev?.getPendingPVs || []),
×
NEW
553
          { id: `${Date.now()}`, offendingProperty, value: offendingValue },
×
554
        ],
NEW
555
      }));
×
556

557
      // NOTE: We refetch after small delay to allow cache to propagate
NEW
558
      setTimeout(refetchPendingPVs, 2000);
×
NEW
559
    },
×
560
    [updatePendingPVs, refetchPendingPVs]
183✔
561
  );
183✔
562

563
  const issueList = useMemo<ErrorDetailsIssue[]>(() => {
183✔
564
    if (!selectedRow || !("errors" in selectedRow) || !("warnings" in selectedRow)) {
34✔
565
      return [];
33✔
566
    }
33✔
567

568
    const allIssues: ErrorDetailsIssue[] = [];
1✔
569
    selectedRow.errors?.forEach((e) => {
34✔
570
      const issue: ErrorDetailsIssue = { severity: "error", message: e.description };
3✔
571

572
      if (e.code === ValidationErrorCodes.INVALID_PERMISSIBLE) {
3!
NEW
573
        const isDisabled = pendingPVs?.getPendingPVs?.some(
×
NEW
574
          (pv) => pv.offendingProperty === e.offendingProperty && pv.value === e.offendingValue
×
NEW
575
        );
×
576

NEW
577
        issue.action = (
×
NEW
578
          <StyledPvButtonWrapper>
×
NEW
579
            <PVRequestButton
×
NEW
580
              onSubmit={handleNewPVRequest}
×
NEW
581
              offendingProperty={e.offendingProperty}
×
NEW
582
              offendingValue={e.offendingValue}
×
NEW
583
              nodeName={selectedRow.type}
×
NEW
584
              disabled={isDisabled}
×
585
            />
NEW
586
          </StyledPvButtonWrapper>
×
587
        );
NEW
588
      }
×
589

590
      allIssues.push(issue);
3✔
591
    });
34✔
592
    selectedRow.warnings?.forEach((w) => {
34✔
NEW
593
      const issue: ErrorDetailsIssue = { severity: "warning", message: w.description };
×
594

NEW
595
      if (w.code === ValidationErrorCodes.UPDATING_DATA && submissionStatus !== "Completed") {
×
NEW
596
        issue.action = (
×
NEW
597
          <NodeComparison
×
NEW
598
            nodeType={selectedRow.type}
×
NEW
599
            submissionID={submissionId}
×
NEW
600
            submittedID={selectedRow.submittedID}
×
601
          />
602
        );
NEW
603
      }
×
604

NEW
605
      allIssues.push(issue);
×
606
    });
34✔
607

608
    return allIssues;
34✔
609
  }, [selectedRow, submissionStatus, pendingPVs, handleNewPVRequest]);
183✔
610

611
  useEffect(() => {
183✔
612
    tableRef.current?.refresh();
20✔
613
  }, [metadataValidationStatus, fileValidationStatus]);
183✔
614

615
  return (
183✔
616
    <>
183✔
617
      <QualityControlFilters
183✔
618
        onChange={handleOnFiltersChange}
183✔
619
        issueType={issueType}
183✔
620
        isAggregated={isAggregated}
183✔
621
      />
622

623
      <QCResultsContext.Provider value={providerValue}>
183✔
624
        <GenericTable
183✔
625
          ref={tableRef}
183✔
626
          columns={currentColumns}
183✔
627
          data={data || []}
183!
628
          total={totalData || 0}
183✔
629
          loading={loading}
183✔
630
          defaultRowsPerPage={20}
183✔
631
          defaultOrder="desc"
183✔
632
          position="both"
183✔
633
          CustomTableHeaderCell={StyledHeaderCell}
183✔
634
          CustomTableBodyCell={StyledTableCell}
183✔
635
          noContentText="No validation issues found. Either no validation has been conducted yet, or all issues have been resolved."
183✔
636
          setItemKey={(item, idx) => `${idx}_${"title" in item ? item?.title : item?.batchID}`}
183✔
637
          onFetchData={handleFetchData}
183✔
638
          AdditionalActions={{
183✔
639
            top: {
183✔
640
              before: (
183✔
641
                <DoubleLabelSwitch
183✔
642
                  leftLabel="Aggregated"
183✔
643
                  rightLabel="Expanded"
183✔
644
                  id="table-state-switch"
183✔
645
                  data-testid="table-view-switch"
183✔
646
                  checked={!isAggregated}
183✔
647
                  onChange={onSwitchToggle}
183✔
648
                  inputProps={{ "aria-label": "Aggregated or Expanded table view switch" }}
183✔
649
                />
650
              ),
651
              after: Actions,
183✔
652
            },
183✔
653
            bottom: {
183✔
654
              after: Actions,
183✔
655
            },
183✔
656
          }}
183✔
657
          containerProps={{ sx: { marginBottom: "8px" } }}
183✔
658
        />
659
      </QCResultsContext.Provider>
183✔
660
      {!isAggregated && (
183✔
661
        <ErrorDetailsDialog
117✔
662
          open={openErrorDialog}
117✔
663
          onClose={() => setOpenErrorDialog(false)}
117✔
664
          preHeader="Data Submission"
117✔
665
          header="Validation Issues"
117✔
666
          postHeader={`For ${titleCase((selectedRow as QCResult)?.type)}${
117✔
667
            (selectedRow as QCResult)?.type?.toLocaleLowerCase() !== "data file" ? " Node" : ""
117!
668
          } ID ${(selectedRow as QCResult)?.submittedID}`}
117✔
669
          issues={issueList}
117✔
670
        />
671
      )}
672
    </>
183✔
673
  );
674
};
183✔
675

676
export default React.memo(QualityControl);
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc