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

CBIIT / crdc-datahub-ui / 9650889762

24 Jun 2024 06:59PM UTC coverage: 31.509% (-0.02%) from 31.532%
9650889762

push

github

GitHub
Merge pull request #404 from CBIIT/CRDCDH-1192

938 of 3718 branches covered (25.23%)

Branch coverage included in aggregate %.

1630 of 4432 relevant lines covered (36.78%)

94.74 hits per line

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

0.0
/src/content/dataSubmissions/QualityControl.tsx
1
import React, { FC, useEffect, useMemo, useRef, useState } from "react";
2
import { useLazyQuery, useQuery } from "@apollo/client";
3
import { useParams } from "react-router-dom";
4
import { cloneDeep, isEqual } from "lodash";
5
import { Box, Button, FormControl, MenuItem, Stack, styled } from "@mui/material";
6
import { Controller, useForm } from "react-hook-form";
7
import { useSnackbar } from "notistack";
8
import {
9
  LIST_BATCHES,
10
  ListBatchesResp,
11
  SUBMISSION_QC_RESULTS,
12
  SUBMISSION_STATS,
13
  SubmissionQCResultsResp,
14
  SubmissionStatsResp,
15
} from "../../graphql";
16
import GenericTable, { Column } from "../../components/DataSubmissions/GenericTable";
17
import { FormatDate, compareNodeStats, titleCase } from "../../utils";
18
import ErrorDialog from "./ErrorDialog";
19
import QCResultsContext from "./Contexts/QCResultsContext";
20
import { ExportValidationButton } from "../../components/DataSubmissions/ExportValidationButton";
21
import DeleteAllOrphanFilesButton from "../../components/DataSubmissions/DeleteAllOrphanFilesButton";
22
import DeleteOrphanFileChip from "../../components/DataSubmissions/DeleteOrphanFileChip";
23
import StyledSelect from "../../components/StyledFormComponents/StyledSelect";
24

25
type FilterForm = {
26
  /**
27
   * The node type to filter by.
28
   *
29
   * @default "All"
30
   */
31
  nodeType: string;
32
  batchID: number | "All";
33
  severity: QCResult["severity"] | "All";
34
};
35

36
const StyledErrorDetailsButton = styled(Button)({
×
37
  display: "inline",
38
  color: "#0B6CB1",
39
  fontFamily: "'Nunito', 'Rubik', sans-serif",
40
  fontSize: "16px",
41
  fontStyle: "normal",
42
  fontWeight: 600,
43
  lineHeight: "19px",
44
  padding: 0,
45
  textDecorationLine: "underline",
46
  textTransform: "none",
47
  "&:hover": {
48
    background: "transparent",
49
    textDecorationLine: "underline",
50
  },
51
});
52

53
const StyledNodeType = styled(Box)({
×
54
  display: "flex",
55
  alignItems: "center",
56
  textTransform: "capitalize",
57
});
58

59
const StyledSeverity = styled(Box)({
×
60
  minHeight: 76.5,
61
  display: "flex",
62
  alignItems: "center",
63
});
64

65
const StyledBreakAll = styled(Box)({
×
66
  wordBreak: "break-all",
67
});
68

69
const StyledFilterContainer = styled(Box)({
×
70
  display: "flex",
71
  alignItems: "center",
72
  justifyContent: "space-between",
73
  marginBottom: "21px",
74
  paddingLeft: "26px",
75
  paddingRight: "35px",
76
});
77

78
const StyledFormControl = styled(FormControl)({
×
79
  minWidth: "231px",
80
});
81

82
const StyledInlineLabel = styled("label")({
×
83
  color: "#083A50",
84
  fontFamily: "'Nunito', 'Rubik', sans-serif",
85
  fontWeight: 700,
86
  fontSize: "16px",
87
  fontStyle: "normal",
88
  lineHeight: "19.6px",
89
  paddingRight: "10px",
90
});
91

92
const StyledIssuesTextWrapper = styled(Box)({
×
93
  whiteSpace: "nowrap",
94
  wordBreak: "break-word",
95
});
96

97
const columns: Column<QCResult>[] = [
×
98
  {
99
    label: "Batch ID",
100
    renderValue: (data) => <StyledBreakAll>{data?.displayID}</StyledBreakAll>,
×
101
    field: "displayID",
102
    default: true,
103
  },
104
  {
105
    label: "Node Type",
106
    renderValue: (data) => <StyledNodeType>{data?.type}</StyledNodeType>,
×
107
    field: "type",
108
  },
109
  {
110
    label: "Submitted Identifier",
111
    renderValue: (data) => <StyledBreakAll>{data?.submittedID}</StyledBreakAll>,
×
112
    field: "submittedID",
113
    sx: {
114
      width: "20%",
115
    },
116
  },
117
  {
118
    label: "Severity",
119
    renderValue: (data) => (
120
      <StyledSeverity color={data?.severity === "Error" ? "#B54717" : "#8D5809"}>
×
121
        {data?.severity}
122
      </StyledSeverity>
123
    ),
124
    field: "severity",
125
  },
126
  {
127
    label: "Validated Date",
128
    renderValue: (data) =>
129
      data?.validatedDate ? `${FormatDate(data?.validatedDate, "MM-DD-YYYY [at] hh:mm A")}` : "",
×
130
    field: "validatedDate",
131
  },
132
  {
133
    label: "Issues",
134
    renderValue: (data) =>
135
      (data?.errors?.length > 0 || data?.warnings?.length > 0) && (
×
136
        <QCResultsContext.Consumer>
137
          {({ submission, handleDeleteOrphanFile, handleOpenErrorDialog }) => (
138
            <Stack direction="row">
×
139
              <StyledIssuesTextWrapper>
140
                <span>
141
                  {data.errors?.length > 0 ? data.errors[0].title : data.warnings[0]?.title}.
×
142
                </span>{" "}
143
                <StyledErrorDetailsButton
144
                  onClick={() => handleOpenErrorDialog && handleOpenErrorDialog(data)}
×
145
                  variant="text"
146
                  disableRipple
147
                  disableTouchRipple
148
                  disableFocusRipple
149
                >
150
                  See details.
151
                </StyledErrorDetailsButton>
152
              </StyledIssuesTextWrapper>
153
              <DeleteOrphanFileChip
154
                submission={submission}
155
                submittedID={data?.submittedID}
156
                onDeleteFile={handleDeleteOrphanFile}
157
              />
158
            </Stack>
159
          )}
160
        </QCResultsContext.Consumer>
161
      ),
162
    sortDisabled: true,
163
    sx: {
164
      width: "38%",
165
    },
166
  },
167
];
168

169
// CSV columns used for exporting table data
170
export const csvColumns = {
×
171
  "Batch ID": (d: QCResult) => d.displayID,
×
172
  "Node Type": (d: QCResult) => d.type,
×
173
  "Submitted Identifier": (d: QCResult) => d.submittedID,
×
174
  Severity: (d: QCResult) => d.severity,
×
175
  "Validated Date": (d: QCResult) => FormatDate(d?.validatedDate, "MM-DD-YYYY [at] hh:mm A", ""),
×
176
  Issues: (d: QCResult) => {
177
    const value = d.errors[0]?.description ?? d.warnings[0]?.description;
×
178

179
    // NOTE: The ErrorMessage descriptions contain non-standard double quotes
180
    // that don't render correctly in Excel. This replaces them with standard double quotes.
181
    return value.replaceAll(/[“”‟〞"]/g, `"`);
×
182
  },
183
};
184

185
type Props = {
186
  submission: Submission;
187
  refreshSubmission: () => void;
188
};
189

190
const QualityControl: FC<Props> = ({ submission, refreshSubmission }: Props) => {
×
191
  const { submissionId } = useParams();
×
192
  const { watch, control } = useForm<FilterForm>();
×
193
  const { enqueueSnackbar } = useSnackbar();
×
194

195
  const [loading, setLoading] = useState<boolean>(false);
×
196
  const [data, setData] = useState<QCResult[]>([]);
×
197
  const [prevData, setPrevData] = useState<FetchListing<QCResult>>(null);
×
198
  const [totalData, setTotalData] = useState(0);
×
199
  const [openErrorDialog, setOpenErrorDialog] = useState<boolean>(false);
×
200
  const [selectedRow, setSelectedRow] = useState<QCResult | null>(null);
×
201
  const tableRef = useRef<TableMethods>(null);
×
202

203
  const errorDescriptions =
204
    selectedRow?.errors?.map((error) => `(Error) ${error.description}`) ?? [];
×
205
  const warningDescriptions =
206
    selectedRow?.warnings?.map((warning) => `(Warning) ${warning.description}`) ?? [];
×
207
  const allDescriptions = [...errorDescriptions, ...warningDescriptions];
×
208

209
  const [submissionQCResults] = useLazyQuery<SubmissionQCResultsResp>(SUBMISSION_QC_RESULTS, {
×
210
    variables: { id: submissionId },
211
    context: { clientName: "backend" },
212
    fetchPolicy: "cache-and-network",
213
  });
214

215
  const { data: batchData } = useQuery<ListBatchesResp>(LIST_BATCHES, {
×
216
    variables: {
217
      submissionID: submissionId,
218
      first: -1,
219
      offset: 0,
220
      partial: true,
221
      orderBy: "displayID",
222
      sortDirection: "asc",
223
    },
224
    context: { clientName: "backend" },
225
    fetchPolicy: "cache-and-network",
226
  });
227

228
  const { data: submissionStats } = useQuery<SubmissionStatsResp>(SUBMISSION_STATS, {
×
229
    variables: { id: submissionId },
230
    context: { clientName: "backend" },
231
    skip: !submissionId,
232
    fetchPolicy: "cache-and-network",
233
  });
234

235
  const nodeTypes = useMemo(
×
236
    () =>
237
      cloneDeep(submissionStats?.submissionStats?.stats)
×
238
        ?.filter((stat) => stat.error > 0 || stat.warning > 0)
×
239
        ?.sort(compareNodeStats)
240
        ?.reverse()
241
        ?.map((stat) => stat.nodeName),
×
242
    [submissionStats?.submissionStats?.stats]
243
  );
244

245
  const handleFetchQCResults = async (fetchListing: FetchListing<QCResult>, force: boolean) => {
×
246
    const { first, offset, sortDirection, orderBy } = fetchListing || {};
×
247
    if (!submissionId) {
×
248
      enqueueSnackbar("Invalid submission ID provided.", { variant: "error" });
×
249
      return;
×
250
    }
251
    if (!force && data?.length > 0 && isEqual(fetchListing, prevData)) {
×
252
      return;
×
253
    }
254

255
    setPrevData(fetchListing);
×
256

257
    try {
×
258
      setLoading(true);
×
259

260
      const nodeType = watch("nodeType");
×
261
      const batchID = watch("batchID");
×
262
      const { data: d, error } = await submissionQCResults({
×
263
        variables: {
264
          submissionID: submissionId,
265
          first,
266
          offset,
267
          sortDirection,
268
          orderBy,
269
          nodeTypes: !nodeType || nodeType === "All" ? undefined : [watch("nodeType")],
×
270
          batchIDs: !batchID || batchID === "All" ? undefined : [watch("batchID")],
×
271
          severities: watch("severity") || "All",
×
272
        },
273
        context: { clientName: "backend" },
274
        fetchPolicy: "no-cache",
275
      });
276
      if (error || !d?.submissionQCResults) {
×
277
        throw new Error("Unable to retrieve submission quality control results.");
×
278
      }
279
      setData(d.submissionQCResults.results);
×
280
      setTotalData(d.submissionQCResults.total);
×
281
    } catch (err) {
282
      enqueueSnackbar(err?.toString(), { variant: "error" });
×
283
    } finally {
284
      setLoading(false);
×
285
    }
286
  };
287

288
  const handleDeleteOrphanFile = (success: boolean) => {
×
289
    if (!success) {
×
290
      return;
×
291
    }
292
    refreshSubmission();
×
293
    tableRef.current?.refresh();
×
294
  };
295

296
  const handleOpenErrorDialog = (data: QCResult) => {
×
297
    setOpenErrorDialog(true);
×
298
    setSelectedRow(data);
×
299
  };
300

301
  const providerValue = useMemo(
×
302
    () => ({
×
303
      submission,
304
      handleDeleteOrphanFile,
305
      handleOpenErrorDialog,
306
    }),
307
    [submission, handleDeleteOrphanFile, handleOpenErrorDialog]
308
  );
309

310
  useEffect(() => {
×
311
    tableRef.current?.setPage(0, true);
×
312
  }, [watch("nodeType"), watch("batchID"), watch("severity")]);
313

314
  useEffect(() => {
×
315
    tableRef.current?.refresh();
×
316
  }, [
317
    submission?.metadataValidationStatus,
318
    submission?.fileValidationStatus,
319
    submission?.crossSubmissionStatus,
320
  ]);
321

322
  return (
×
323
    <>
324
      <StyledFilterContainer>
325
        <Stack direction="row" justifyContent="flex-start" alignItems="center">
326
          <StyledInlineLabel htmlFor="batchID-filter">Batch ID</StyledInlineLabel>
327
          <StyledFormControl>
328
            <Controller
329
              name="batchID"
330
              control={control}
331
              render={({ field }) => (
332
                <StyledSelect
×
333
                  {...field}
334
                  defaultValue="All"
335
                  value={field.value || "All"}
×
336
                  /* zIndex has to be higher than the SuspenseLoader to avoid cropping */
337
                  MenuProps={{ disablePortal: true, sx: { zIndex: 99999 } }}
338
                  inputProps={{ id: "batchID-filter" }}
339
                >
340
                  <MenuItem value="All">All</MenuItem>
341
                  {batchData?.listBatches?.batches?.map((batch) => (
342
                    <MenuItem key={batch._id} value={batch._id}>
×
343
                      {batch.displayID}
344
                      {` (${FormatDate(batch.createdAt, "MM/DD/YYYY")})`}
345
                    </MenuItem>
346
                  ))}
347
                </StyledSelect>
348
              )}
349
            />
350
          </StyledFormControl>
351
        </Stack>
352

353
        <Stack direction="row" justifyContent="flex-start" alignItems="center">
354
          <StyledInlineLabel htmlFor="nodeType-filter">Node Type</StyledInlineLabel>
355
          <StyledFormControl>
356
            <Controller
357
              name="nodeType"
358
              control={control}
359
              render={({ field }) => (
360
                <StyledSelect
×
361
                  {...field}
362
                  defaultValue="All"
363
                  value={field.value || "All"}
×
364
                  /* zIndex has to be higher than the SuspenseLoader to avoid cropping */
365
                  MenuProps={{ disablePortal: true, sx: { zIndex: 99999 } }}
366
                  inputProps={{ id: "nodeType-filter" }}
367
                >
368
                  <MenuItem value="All">All</MenuItem>
369
                  {nodeTypes?.map((nodeType) => (
370
                    <MenuItem key={nodeType} value={nodeType}>
×
371
                      {nodeType.toLowerCase()}
372
                    </MenuItem>
373
                  ))}
374
                </StyledSelect>
375
              )}
376
            />
377
          </StyledFormControl>
378
        </Stack>
379

380
        <Stack direction="row" justifyContent="flex-start" alignItems="center">
381
          <StyledInlineLabel htmlFor="severity-filter">Severity</StyledInlineLabel>
382
          <StyledFormControl>
383
            <Controller
384
              name="severity"
385
              control={control}
386
              render={({ field }) => (
387
                <StyledSelect
×
388
                  {...field}
389
                  defaultValue="All"
390
                  value={field.value || "All"}
×
391
                  /* zIndex has to be higher than the SuspenseLoader to avoid cropping */
392
                  MenuProps={{ disablePortal: true, sx: { zIndex: 99999 } }}
393
                  inputProps={{ id: "severity-filter" }}
394
                >
395
                  <MenuItem value="All">All</MenuItem>
396
                  <MenuItem value="Error">Error</MenuItem>
397
                  <MenuItem value="Warning">Warning</MenuItem>
398
                </StyledSelect>
399
              )}
400
            />
401
          </StyledFormControl>
402
        </Stack>
403
      </StyledFilterContainer>
404
      <QCResultsContext.Provider value={providerValue}>
405
        <GenericTable
406
          ref={tableRef}
407
          columns={columns}
408
          data={data || []}
×
409
          total={totalData || 0}
×
410
          loading={loading}
411
          defaultRowsPerPage={20}
412
          defaultOrder="desc"
413
          position="both"
414
          setItemKey={(item, idx) => `${idx}_${item.batchID}_${item.submittedID}`}
×
415
          onFetchData={handleFetchQCResults}
416
          AdditionalActions={
417
            <Stack direction="row" alignItems="center" gap="8px" marginRight="37px">
418
              <ExportValidationButton
419
                submission={submission}
420
                fields={csvColumns}
421
                disabled={totalData <= 0}
422
              />
423
              <DeleteAllOrphanFilesButton
424
                submission={submission}
425
                disabled={!submission?.fileErrors?.length}
426
                onDelete={handleDeleteOrphanFile}
427
              />
428
            </Stack>
429
          }
430
          containerProps={{ sx: { marginBottom: "8px" } }}
431
        />
432
      </QCResultsContext.Provider>
433
      <ErrorDialog
434
        open={openErrorDialog}
435
        onClose={() => setOpenErrorDialog(false)}
×
436
        header={null}
437
        title="Validation Issues"
438
        nodeInfo={`For ${titleCase(selectedRow?.type)}${
439
          selectedRow?.type?.toLocaleLowerCase() !== "data file" ? " Node" : ""
×
440
        } ID ${selectedRow?.submittedID}`}
441
        errors={allDescriptions}
442
        errorCount={`${allDescriptions?.length || 0} ${
×
443
          allDescriptions?.length === 1 ? "ISSUE" : "ISSUES"
×
444
        }`}
445
      />
446
    </>
447
  );
448
};
449

450
export default React.memo<Props>(QualityControl, (prevProps, nextProps) =>
451
  isEqual(prevProps, nextProps)
×
452
);
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