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

CBIIT / crdc-datahub-ui / 14229012485

28 Mar 2025 05:16PM UTC coverage: 60.279% (+6.1%) from 54.222%
14229012485

push

github

web-flow
Merge pull request #655 from CBIIT/3.2.0

3.2.0 Release

3137 of 5624 branches covered (55.78%)

Branch coverage included in aggregate %.

991 of 1319 new or added lines in 98 files covered. (75.13%)

33 existing lines in 14 files now uncovered.

4372 of 6833 relevant lines covered (63.98%)

147.15 hits per line

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

95.12
/src/components/DataSubmissions/ExportValidationButton.tsx
1
import { useState } from "react";
2
import { useLazyQuery } from "@apollo/client";
3
import { IconButtonProps, IconButton, styled } from "@mui/material";
4
import { CloudDownload } from "@mui/icons-material";
5
import { useSnackbar } from "notistack";
6
import dayjs from "dayjs";
7
import { unparse } from "papaparse";
8
import StyledFormTooltip from "../StyledFormComponents/StyledTooltip";
9
import {
10
  AGGREGATED_SUBMISSION_QC_RESULTS,
11
  AggregatedSubmissionQCResultsInput,
12
  AggregatedSubmissionQCResultsResp,
13
  SUBMISSION_QC_RESULTS,
14
  SubmissionQCResultsResp,
15
} from "../../graphql";
16
import { downloadBlob, filterAlphaNumeric, Logger, unpackValidationSeverities } from "../../utils";
17

18
export type Props = {
19
  /**
20
   * The full Data Submission object to export validation results for
21
   */
22
  submission: Submission;
23
  /**
24
   * The K:V pair of the fields that should be exported where
25
   * `key` is the column header and `value` is a function
26
   * that generates the exportable value
27
   *
28
   * @example { "Batch ID": (d) => d.displayID }
29
   */
30
  fields: Record<string, (row: QCResult | AggregatedQCResult) => string | number>;
31
  /**
32
   * Tells the component whether to export the "aggregated" or the "expanded" data.
33
   * @default false
34
   */
35
  isAggregated?: boolean;
36
} & IconButtonProps;
37

38
const StyledIconButton = styled(IconButton)({
4✔
39
  color: "#606060",
40
});
41

42
const StyledTooltip = styled(StyledFormTooltip)({
4✔
43
  "& .MuiTooltip-tooltip": {
44
    color: "#000000",
45
  },
46
});
47

48
/**
49
 * Provides the button and supporting functionality to export the validation results of a submission.
50
 *
51
 * @returns {React.FC} The export validation button.
52
 */
53
export const ExportValidationButton: React.FC<Props> = ({
4✔
54
  submission,
55
  fields,
56
  isAggregated = false,
58✔
57
  disabled,
58
  ...buttonProps
59
}: Props) => {
60
  const { enqueueSnackbar } = useSnackbar();
536✔
61
  const [loading, setLoading] = useState<boolean>(false);
536✔
62

63
  const [getSubmissionQCResults] = useLazyQuery<SubmissionQCResultsResp>(SUBMISSION_QC_RESULTS, {
536✔
64
    context: { clientName: "backend" },
65
    fetchPolicy: "no-cache",
66
  });
67

68
  const [getAggregatedSubmissionQCResults] = useLazyQuery<
536✔
69
    AggregatedSubmissionQCResultsResp,
70
    AggregatedSubmissionQCResultsInput
71
  >(AGGREGATED_SUBMISSION_QC_RESULTS, {
72
    context: { clientName: "backend" },
73
    fetchPolicy: "no-cache",
74
  });
75

76
  /**
77
   * Helper to generate CSV and trigger download.
78
   * This function:
79
   *  1) Optionally unpacks severities if not aggregated
80
   *  2) Uses the given `fields` to generate CSV rows
81
   *  3) Calls `downloadBlob` to save the CSV file
82
   *
83
   * @returns {void}
84
   */
85
  const createCSVAndDownload = (
536✔
86
    rows: (QCResult | AggregatedQCResult)[],
87
    filename: string,
88
    isAggregated: boolean
89
  ): void => {
90
    try {
26✔
91
      let finalRows = rows;
26✔
92

93
      if (!isAggregated) {
26✔
94
        finalRows = unpackValidationSeverities<QCResult>(rows as QCResult[]);
22✔
95
      }
96

97
      const fieldEntries = Object.entries(fields);
24✔
98
      const csvArray = finalRows.map((row) => {
24✔
99
        const csvRow: Record<string, string | number> = {};
48✔
100
        fieldEntries.forEach(([header, fn]) => {
48✔
101
          csvRow[header] = fn(row) ?? "";
100✔
102
        });
103
        return csvRow;
48✔
104
      });
105

106
      downloadBlob(unparse(csvArray), filename, "text/csv");
24✔
107
    } catch (err) {
108
      enqueueSnackbar(`Unable to export validation results. Error: ${err}`, { variant: "error" });
2✔
109
    }
110
  };
111

112
  /**
113
   *  Creates a file name by using the submission name, filtering by alpha-numeric characters,
114
   * then adding the date and time
115
   *
116
   * @returns {string} A formatted file name for the exported file
117
   */
118
  const createFileName = (): string => {
536✔
119
    const filteredName = filterAlphaNumeric(submission.name?.trim()?.replaceAll(" ", "-"), "-");
26✔
120
    return `${filteredName}-${dayjs().format("YYYY-MM-DDTHHmmss")}.csv`;
26✔
121
  };
122

123
  /**
124
   *  Will retrieve all of the aggregated submission QC results to
125
   * construct and download a CSV file
126
   *
127
   *
128
   * @returns {Promise<void>}
129
   */
130
  const handleAggregatedExportSetup = async (): Promise<void> => {
536✔
131
    setLoading(true);
10✔
132

133
    try {
10✔
134
      const { data, error } = await getAggregatedSubmissionQCResults({
10✔
135
        variables: {
136
          submissionID: submission?._id,
137
          partial: false,
138
          first: -1,
139
          orderBy: "title",
140
          sortDirection: "asc",
141
        },
142
      });
143

144
      if (error || !data?.aggregatedSubmissionQCResults?.results) {
10✔
145
        enqueueSnackbar("Unable to retrieve submission aggregated quality control results.", {
4✔
146
          variant: "error",
147
        });
148
        return;
4✔
149
      }
150

151
      if (!data.aggregatedSubmissionQCResults.results.length) {
6✔
152
        enqueueSnackbar("There are no aggregated validation results to export.", {
2✔
153
          variant: "error",
154
        });
155
        return;
2✔
156
      }
157

158
      createCSVAndDownload(data.aggregatedSubmissionQCResults.results, createFileName(), true);
4✔
159
    } catch (err) {
NEW
160
      enqueueSnackbar(`Unable to export aggregated validation results. Error: ${err}`, {
×
161
        variant: "error",
162
      });
NEW
163
      Logger.error(
×
164
        `ExportValidationButton: Unable to export aggregated validation results. Error: ${err}`
165
      );
166
    } finally {
167
      setLoading(false);
10✔
168
    }
169
  };
170

171
  /**
172
   *  Will retrieve all of the expanded submission QC results to
173
   * construct and download a CSV file
174
   *
175
   *
176
   * @returns {Promise<void>}
177
   */
178
  const handleExpandedExportSetup = async () => {
536✔
179
    setLoading(true);
28✔
180

181
    try {
28✔
182
      const { data, error } = await getSubmissionQCResults({
28✔
183
        variables: {
184
          id: submission?._id,
185
          sortDirection: "asc",
186
          orderBy: "displayID",
187
          first: -1,
188
          offset: 0,
189
        },
190
      });
191

192
      if (error || !data?.submissionQCResults?.results) {
28✔
193
        enqueueSnackbar("Unable to retrieve submission quality control results.", {
4✔
194
          variant: "error",
195
        });
196
        return;
4✔
197
      }
198

199
      if (!data.submissionQCResults.results.length) {
24✔
200
        enqueueSnackbar("There are no validation results to export.", { variant: "error" });
2✔
201
        return;
2✔
202
      }
203

204
      createCSVAndDownload(data.submissionQCResults.results, createFileName(), false);
22✔
205
    } catch (err) {
NEW
206
      enqueueSnackbar(`Unable to export expanded validation results. Error: ${err}`, {
×
207
        variant: "error",
208
      });
NEW
209
      Logger.error(
×
210
        `ExportValidationButton: Unable to export expanded validation results. Error: ${err}`
211
      );
212
    } finally {
213
      setLoading(false);
28✔
214
    }
215
  };
216

217
  /**
218
   * Click handler that triggers the setup
219
   * for aggregated or expanded CSV file exporting
220
   */
221
  const handleClick = async () => {
536✔
222
    if (isAggregated) {
38✔
223
      handleAggregatedExportSetup();
10✔
224
      return;
10✔
225
    }
226

227
    handleExpandedExportSetup();
28✔
228
  };
229

230
  return (
536✔
231
    <StyledTooltip
232
      title={
233
        <span>
234
          Export all validation issues for this data <br />
235
          submission to a CSV file
236
        </span>
237
      }
238
      placement="top"
239
      data-testid="export-validation-tooltip"
240
    >
241
      <span>
242
        <StyledIconButton
243
          onClick={handleClick}
244
          disabled={loading || disabled}
498✔
245
          data-testid="export-validation-button"
246
          aria-label="Export validation results"
247
          {...buttonProps}
248
        >
249
          <CloudDownload />
250
        </StyledIconButton>
251
      </span>
252
    </StyledTooltip>
253
  );
254
};
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