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

CBIIT / crdc-datahub-ui / 8421544006

25 Mar 2024 02:15PM UTC coverage: 19.152%. First build
8421544006

Pull #313

github

web-flow
Merge b68a9fc65 into 152d41ba1
Pull Request #313: CRDCDH-882 Validation Results CSV Export & Jest Improvements

452 of 3367 branches covered (13.42%)

Branch coverage included in aggregate %.

45 of 56 new or added lines in 4 files covered. (80.36%)

944 of 3922 relevant lines covered (24.07%)

94.27 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 { FC, useEffect, useMemo, useRef, useState } from "react";
2
import { useLazyQuery, useQuery } from "@apollo/client";
3
import { useParams } from "react-router-dom";
4
import { isEqual } from "lodash";
5
import { Box, Button, FormControl, MenuItem, Select, styled } from "@mui/material";
6
import { Controller, useForm } from 'react-hook-form';
7
import { useSnackbar } from 'notistack';
8
import { LIST_BATCHES, LIST_NODE_TYPES, ListBatchesResp, ListNodeTypesResp, SUBMISSION_QC_RESULTS, SubmissionQCResultsResp } from "../../graphql";
9
import GenericTable, { Column, FetchListing, TableMethods } from "../../components/DataSubmissions/GenericTable";
10
import { FormatDate, capitalizeFirstLetter } from "../../utils";
11
import ErrorDialog from "./ErrorDialog";
12
import QCResultsContext from "./Contexts/QCResultsContext";
13

14
type FilterForm = {
15
  nodeType: string | "All";
16
  batchID: number | "All";
17
  severity: QCResult["severity"] | "All";
18
};
19

20
const StyledErrorDetailsButton = styled(Button)({
×
21
  display: "inline",
22
  color: "#0B6CB1",
23
  fontFamily: "'Nunito', 'Rubik', sans-serif",
24
  fontSize: "16px",
25
  fontStyle: "normal",
26
  fontWeight: 600,
27
  lineHeight: "19px",
28
  padding: 0,
29
  textDecorationLine: "underline",
30
  textTransform: "none",
31
  "&:hover": {
32
    background: "transparent",
33
    textDecorationLine: "underline",
34
  },
35
});
36

37
const StyledNodeType = styled(Box)({
×
38
  display: "flex",
39
  alignItems: "center",
40
  textTransform: "capitalize"
41
});
42

43
const StyledSeverity = styled(Box)({
×
44
  minHeight: 76.5,
45
  display: "flex",
46
  alignItems: "center",
47
});
48

49
const StyledBreakAll = styled(Box)({
×
50
  wordBreak: "break-all"
51
});
52

53
const StyledFilterContainer = styled(Box)({
×
54
  display: "flex",
55
  alignItems: "center",
56
  justifyContent: "flex-start",
57
  marginBottom: "19px",
58
  paddingLeft: "24px",
59
});
60

61
const StyledFormControl = styled(FormControl)({
×
62
  margin: "10px",
63
  marginRight: "15px",
64
  minWidth: "250px",
65
});
66

67
const StyledInlineLabel = styled('label')({
×
68
  padding: "0 10px",
69
  fontWeight: "700"
70
});
71

72
const baseTextFieldStyles = {
×
73
  borderRadius: "8px",
74
  "& .MuiInputBase-input": {
75
    fontWeight: 400,
76
    fontSize: "16px",
77
    fontFamily: "'Nunito', 'Rubik', sans-serif",
78
    padding: "10px",
79
    height: "20px",
80
  },
81
  "& .MuiOutlinedInput-notchedOutline": {
82
    borderColor: "#6B7294",
83
  },
84
  "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
85
    border: "1px solid #209D7D",
86
    boxShadow: "2px 2px 4px 0px rgba(38, 184, 147, 0.10), -1px -1px 6px 0px rgba(38, 184, 147, 0.20)",
87
  },
88
  "& .Mui-disabled": {
89
    cursor: "not-allowed",
90
  },
91
  "& .MuiList-root": {
92
    padding: "0 !important",
93
  },
94
  "& .MuiMenuItem-root.Mui-selected": {
95
    background: "#3E7E6D !important",
96
    color: "#FFFFFF !important",
97
  },
98
  "& .MuiMenuItem-root:hover": {
99
    background: "#D5EDE5",
100
  },
101
};
102

103
const StyledSelect = styled(Select)(baseTextFieldStyles);
×
104

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

157
// CSV columns used for exporting table data
NEW
158
export const csvColumns = {
×
NEW
159
  "Batch ID": (d: QCResult) => d.displayID,
×
NEW
160
  "Node Type": (d: QCResult) => d.type,
×
NEW
161
  "Submitted Identifier": (d: QCResult) => d.submittedID,
×
NEW
162
  Severity: (d: QCResult) => d.severity,
×
NEW
163
  "Validated Date": (d: QCResult) => FormatDate(d?.validatedDate, "MM-DD-YYYY [at] hh:mm A", ""),
×
NEW
164
  Issues: (d: QCResult) => (d.errors?.length > 0 ? d.errors[0].description : d.warnings[0]?.description),
×
165
};
166

167
const QualityControl: FC = () => {
×
168
  const { submissionId } = useParams();
×
169
  const { watch, control } = useForm<FilterForm>();
×
NEW
170
  const { enqueueSnackbar } = useSnackbar();
×
171

172
  const [loading, setLoading] = useState<boolean>(false);
×
173
  const [data, setData] = useState<QCResult[]>([]);
×
174
  const [prevData, setPrevData] = useState<FetchListing<QCResult>>(null);
×
175
  const [totalData, setTotalData] = useState(0);
×
176
  const [openErrorDialog, setOpenErrorDialog] = useState<boolean>(false);
×
177
  const [selectedRow, setSelectedRow] = useState<QCResult | null>(null);
×
178
  const tableRef = useRef<TableMethods>(null);
×
179

180
  const errorDescriptions = selectedRow?.errors?.map((error) => `(Error) ${error.description}`) ?? [];
×
181
  const warningDescriptions = selectedRow?.warnings?.map((warning) => `(Warning) ${warning.description}`) ?? [];
×
182
  const allDescriptions = [...errorDescriptions, ...warningDescriptions];
×
183

184
  const [submissionQCResults] = useLazyQuery<SubmissionQCResultsResp>(SUBMISSION_QC_RESULTS, {
×
185
    variables: { id: submissionId },
186
    context: { clientName: 'backend' },
187
    fetchPolicy: 'cache-and-network',
188
  });
189

190
  const { data: batchData } = useQuery<ListBatchesResp>(LIST_BATCHES, {
×
191
    variables: {
192
      submissionID: submissionId,
193
      first: -1,
194
      offset: 0,
195
      partial: true,
196
      orderBy: "displayID",
197
      sortDirection: "asc",
198
    },
199
    context: { clientName: 'backend' },
200
    fetchPolicy: 'cache-and-network',
201
  });
202

203
  const { data: nodeTypes } = useQuery<ListNodeTypesResp>(LIST_NODE_TYPES, {
×
204
    variables: { _id: submissionId, },
205
    context: { clientName: 'backend' },
206
    fetchPolicy: 'cache-and-network',
207
  });
208

209
  const handleFetchQCResults = async (fetchListing: FetchListing<QCResult>, force: boolean) => {
×
210
    const { first, offset, sortDirection, orderBy } = fetchListing || {};
×
211
    if (!submissionId) {
×
NEW
212
      enqueueSnackbar("Invalid submission ID provided.", { variant: "error" });
×
213
      return;
×
214
    }
215
    if (!force && data?.length > 0 && isEqual(fetchListing, prevData)) {
×
216
      return;
×
217
    }
218

219
    setPrevData(fetchListing);
×
220

221
    try {
×
222
      setLoading(true);
×
223

224
      const nodeType = watch("nodeType");
×
225
      const batchID = watch("batchID");
×
226
      const { data: d, error } = await submissionQCResults({
×
227
        variables: {
228
          submissionID: submissionId,
229
          first,
230
          offset,
231
          sortDirection,
232
          orderBy,
233
          nodeTypes: !nodeType || nodeType === "All" ? undefined : [watch("nodeType")],
×
234
          batchIDs: !batchID || batchID === "All" ? undefined : [watch("batchID")],
×
235
          severities: watch("severity") || "All",
×
236
        },
237
        context: { clientName: 'backend' },
238
        fetchPolicy: 'no-cache'
239
      });
240
      if (error || !d?.submissionQCResults) {
×
241
        throw new Error("Unable to retrieve submission quality control results.");
×
242
        return;
×
243
      }
244
      setData(d.submissionQCResults.results);
×
245
      setTotalData(d.submissionQCResults.total);
×
246
    } catch (err) {
NEW
247
      enqueueSnackbar(err?.toString(), { variant: "error" });
×
248
    } finally {
249
      setLoading(false);
×
250
    }
251
  };
252

253
  const handleOpenErrorDialog = (data: QCResult) => {
×
254
    setOpenErrorDialog(true);
×
255
    setSelectedRow(data);
×
256
  };
257

258
  const providerValue = useMemo(() => ({
×
259
    handleOpenErrorDialog
260
  }), [handleOpenErrorDialog]);
261

262
  useEffect(() => {
×
263
    tableRef.current?.setPage(0, true);
×
264
  }, [watch("nodeType"), watch("batchID"), watch("severity")]);
265

266
  return (
×
267
    <>
268
      <StyledFilterContainer>
269
        <StyledInlineLabel htmlFor="nodeType-filter">Node Type</StyledInlineLabel>
270
        <StyledFormControl>
271
          <Controller
272
            name="nodeType"
273
            control={control}
274
            render={({ field }) => (
275
              <StyledSelect
×
276
                {...field}
277
                defaultValue="All"
278
                value={field.value || "All"}
×
279
                MenuProps={{ disablePortal: true }}
280
                inputProps={{ id: "nodeType-filter" }}
281
              >
282
                <MenuItem value="All">All</MenuItem>
283
                {nodeTypes?.listSubmissionNodeTypes?.map((nodeType) => (
284
                  <MenuItem key={nodeType} value={nodeType}>{nodeType}</MenuItem>
×
285
                ))}
286
              </StyledSelect>
287
            )}
288
          />
289
        </StyledFormControl>
290
        <StyledInlineLabel htmlFor="batchID-filter">Batch ID</StyledInlineLabel>
291
        <StyledFormControl>
292
          <Controller
293
            name="batchID"
294
            control={control}
295
            render={({ field }) => (
296
              <StyledSelect
×
297
                {...field}
298
                defaultValue="All"
299
                value={field.value || "All"}
×
300
                MenuProps={{ disablePortal: true }}
301
                inputProps={{ id: "batchID-filter" }}
302
              >
303
                <MenuItem value="All">All</MenuItem>
304
                {batchData?.listBatches?.batches?.map((batch) => (
305
                  <MenuItem key={batch._id} value={batch._id}>
×
306
                    {batch.displayID}
307
                    {` (${FormatDate(batch.createdAt, "MM/DD/YYYY")})`}
308
                  </MenuItem>
309
                ))}
310
              </StyledSelect>
311
            )}
312
          />
313
        </StyledFormControl>
314
        <StyledInlineLabel htmlFor="severity-filter">Severity</StyledInlineLabel>
315
        <StyledFormControl>
316
          <Controller
317
            name="severity"
318
            control={control}
319
            render={({ field }) => (
320
              <StyledSelect
×
321
                {...field}
322
                defaultValue="All"
323
                value={field.value || "All"}
×
324
                MenuProps={{ disablePortal: true }}
325
                inputProps={{ id: "severity-filter" }}
326
              >
327
                <MenuItem value="All">All</MenuItem>
328
                <MenuItem value="Error">Error</MenuItem>
329
                <MenuItem value="Warning">Warning</MenuItem>
330
              </StyledSelect>
331
            )}
332
          />
333
        </StyledFormControl>
334
      </StyledFilterContainer>
335
      <QCResultsContext.Provider value={providerValue}>
336
        <GenericTable
337
          ref={tableRef}
338
          columns={columns}
339
          data={data || []}
×
340
          total={totalData || 0}
×
341
          loading={loading}
342
          defaultRowsPerPage={20}
343
          defaultOrder="desc"
344
          setItemKey={(item, idx) => `${idx}_${item.batchID}_${item.submittedID}`}
×
345
          onFetchData={handleFetchQCResults}
346
        />
347
      </QCResultsContext.Provider>
348
      <ErrorDialog
349
        open={openErrorDialog}
350
        onClose={() => setOpenErrorDialog(false)}
×
351
        header={null}
352
        title={`Validation Issues for ${capitalizeFirstLetter(selectedRow?.type)} Node ID ${selectedRow?.submittedID}.`}
353
        errors={allDescriptions}
354
        errorCount={`${allDescriptions?.length || 0} ${allDescriptions?.length === 1 ? "ISSUE" : "ISSUES"}`}
×
355
      />
356
    </>
357
  );
358
};
359

360
export default QualityControl;
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

© 2025 Coveralls, Inc