• 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

98.65
/src/components/DataExplorerStudyExport/index.tsx
1
import { useLazyQuery } from "@apollo/client";
1✔
2
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
1✔
3
import {
1✔
4
  IconButtonProps,
5
  IconButton,
6
  styled,
7
  Box,
8
  ClickAwayListener,
9
  MenuItem,
10
  MenuList,
11
  Paper,
12
  Popper,
13
  Stack,
14
  Typography,
15
} from "@mui/material";
16
import dayjs from "dayjs";
1✔
17
import { useSnackbar } from "notistack";
1✔
18
import { unparse } from "papaparse";
1✔
19
import { memo, useRef, useState } from "react";
1✔
20

21
import CloseIconSvg from "@/assets/icons/close_icon.svg?react";
1✔
22
import DownloadIconSvg from "@/assets/icons/download_icon.svg?react";
1✔
23
import type { Column } from "@/components/GenericTable";
24
import {
1✔
25
  DOWNLOAD_ALL_RELEASED_NODES,
26
  DownloadAllReleasedNodesResp,
27
  DownloadAllReleaseNodesInput,
28
  LIST_RELEASED_DATA_RECORDS,
29
  ListReleasedDataRecordsInput,
30
  ListReleasedDataRecordsResponse,
31
} from "@/graphql";
32
import { downloadBlob, fetchAllData, Logger } from "@/utils";
1✔
33

34
export type DataExplorerStudyExportProps = {
35
  /**
36
   * The `_id` of the study to export data for.
37
   */
38
  studyId: string;
39
  /**
40
   * The display name of the study to export data for (e.g. "My Study").
41
   * Included in the filename of the exported TSV.
42
   */
43
  studyDisplayName: string;
44
  /**
45
   * The node type to export data for (e.g. "participant").
46
   */
47
  nodeType: string;
48
  /**
49
   * The display name of the data commons to filter exported data by (e.g. "GC").
50
   */
51
  dataCommonsDisplayName: string;
52
  /**
53
   * The visible columns that should be included in the export TSV.
54
   */
55
  columns: Column<ListReleasedDataRecordsResponse["listReleasedDataRecords"]["nodes"][number]>[];
56
} & IconButtonProps;
57

58
const StyledIconButton = styled(IconButton)({
1✔
59
  padding: 0,
1✔
60
  paddingLeft: "3px",
1✔
61
  marginLeft: "4px",
1✔
62
});
1✔
63

64
const StyledExpandMoreIcon = styled(ExpandMoreIcon)({
1✔
65
  ".Mui-disabled &": {
1✔
66
    color: "#BBBBBB",
1✔
67
  },
1✔
68
  color: "#000",
1✔
69
  fontSize: "18px",
1✔
70
  alignSelf: "flex-end",
1✔
71
});
1✔
72

73
const StyledDownloadIcon = styled(DownloadIconSvg)({
1✔
74
  ".Mui-disabled &": {
1✔
75
    color: "#BBBBBB",
1✔
76
  },
1✔
77
  color: "#346798",
1✔
78
  width: "24px",
1✔
79
  height: "24px",
1✔
80
});
1✔
81

82
const StyledPaper = styled(Paper)({
1✔
83
  border: "1px solid #000000",
1✔
84
  borderRadius: "8px",
1✔
85
});
1✔
86

87
const StyledMenuList = styled(MenuList)({
1✔
88
  paddingBottom: 0,
1✔
89
});
1✔
90

91
const StyledMenuHeader = styled(Stack)({
1✔
92
  position: "relative",
1✔
93
  padding: "11px 17px",
1✔
94
  paddingBottom: "0",
1✔
95
});
1✔
96

97
const StyledMenuTitle = styled(Typography)({
1✔
98
  fontWeight: "700",
1✔
99
  fontSize: "16px",
1✔
100
  color: "#083A50",
1✔
101
});
1✔
102

103
const StyledPopperCloseButton = styled(IconButton)(() => ({
1✔
104
  padding: "10px",
25✔
105
  "& svg": {
25✔
106
    color: "#44627C",
25✔
107
  },
25✔
108
}));
1✔
109

110
const StyledMenuItem = styled(MenuItem)({
1✔
111
  height: "50px",
1✔
112
  borderTop: "1px solid #CCCCCC",
1✔
113
  color: "#0A4A6D",
1✔
114
  fontWeight: "500",
1✔
115
});
1✔
116

117
const StyledExportFormat = styled(Typography)({
1✔
118
  fontSize: "9px",
1✔
119
  color: "#0A4A6D",
1✔
120
  fontWeight: "400",
1✔
121
  marginLeft: "5px",
1✔
122
});
1✔
123

124
/**
125
 * Provides the button and supporting functionality to export the
126
 * released metadata for a given study.
127
 *
128
 * @returns The button to export the released metadata
129
 */
130
const DataExplorerStudyExport: React.FC<DataExplorerStudyExportProps> = ({
1✔
131
  studyId,
112✔
132
  studyDisplayName,
112✔
133
  nodeType,
112✔
134
  dataCommonsDisplayName,
112✔
135
  columns,
112✔
136
  ...buttonProps
112✔
137
}: DataExplorerStudyExportProps) => {
112✔
138
  const { enqueueSnackbar } = useSnackbar();
112✔
139

140
  const [loading, setLoading] = useState<boolean>(false);
112✔
141
  const [menuOpen, setMenuOpen] = useState(false);
112✔
142
  const anchorRef = useRef<HTMLButtonElement>(null);
112✔
143

144
  const [listReleasedDataRecords] = useLazyQuery<
112✔
145
    ListReleasedDataRecordsResponse,
146
    ListReleasedDataRecordsInput
147
  >(LIST_RELEASED_DATA_RECORDS, {
112✔
148
    context: { clientName: "backend" },
112✔
149
    fetchPolicy: "cache-and-network",
112✔
150
  });
112✔
151

152
  const [downloadAllReleasedNodes] = useLazyQuery<
112✔
153
    DownloadAllReleasedNodesResp,
154
    DownloadAllReleaseNodesInput
155
  >(DOWNLOAD_ALL_RELEASED_NODES, {
112✔
156
    context: { clientName: "backend" },
112✔
157
    fetchPolicy: "no-cache",
112✔
158
  });
112✔
159

160
  const handleToggle = () => {
112✔
161
    setMenuOpen((prevOpen) => !prevOpen);
25✔
162
  };
25✔
163

164
  const handleClose = (event: Event | React.SyntheticEvent) => {
112✔
165
    if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
1!
NEW
166
      return;
×
NEW
167
    }
×
168

169
    setMenuOpen(false);
1✔
170
  };
1✔
171

172
  const handleClickSelected = async () => {
112✔
173
    setLoading(true);
14✔
174
    setMenuOpen(false);
14✔
175

176
    enqueueSnackbar("Downloading the requested metadata file. This may take a moment...", {
14✔
177
      variant: "default",
14✔
178
    });
14✔
179

180
    try {
14✔
181
      const data = await fetchAllData<
14✔
182
        ListReleasedDataRecordsResponse,
183
        ListReleasedDataRecordsInput,
184
        Record<string, unknown>
185
      >(
186
        listReleasedDataRecords,
14✔
187
        { dataCommonsDisplayName, studyId, nodeType },
14✔
188
        (data) => data.listReleasedDataRecords.nodes,
14✔
189
        (data) => data.listReleasedDataRecords.total,
14✔
190
        { pageSize: 5000, total: Infinity }
14✔
191
      );
14✔
192

193
      if (!data?.length) {
14✔
194
        throw new Error("No data returned from fetch");
1✔
195
      }
1✔
196

197
      const filename = `${studyDisplayName}_${nodeType}_${dayjs().format("YYYYMMDDHHmmss")}.tsv`;
8✔
198
      const finalData = data.map((item) => {
8✔
199
        const newItem: Record<string, unknown> = {};
15,006✔
200
        columns.forEach((col) => {
15,006✔
201
          newItem[col.field] = item[col?.field] || "";
14✔
202
        });
15,006✔
203
        return newItem;
15,006✔
204
      });
8✔
205

206
      downloadBlob(unparse(finalData, { delimiter: "\t" }), filename, "text/tab-separated-values");
8✔
207
    } catch (err) {
14✔
208
      Logger.error("Error during study TSV generation", err);
5✔
209
      enqueueSnackbar("Failed to generate the TSV for the selected node.", {
5✔
210
        variant: "error",
5✔
211
      });
5✔
212
    } finally {
14✔
213
      setLoading(false);
13✔
214
    }
13✔
215
  };
14✔
216

217
  const handleClickFull = async () => {
112✔
218
    setLoading(true);
6✔
219
    setMenuOpen(false);
6✔
220

221
    enqueueSnackbar("Downloading the requested metadata file. This may take a moment...", {
6✔
222
      variant: "default",
6✔
223
    });
6✔
224

225
    try {
6✔
226
      const { data, error } = await downloadAllReleasedNodes({
6✔
227
        variables: { studyId, dataCommonsDisplayName },
6✔
228
      });
6✔
229

230
      if (error) {
6✔
231
        throw error;
2✔
232
      }
2✔
233
      if (!data?.downloadAllReleasedNodes) {
6✔
234
        throw new Error("Oops! The API did not return a download link.");
2✔
235
      }
2✔
236

237
      window.open(data.downloadAllReleasedNodes, "_blank", "noopener,noreferrer");
1✔
238
    } catch (err) {
6✔
239
      Logger.error("Error during study download", err);
4✔
240
      enqueueSnackbar(err?.message, {
4✔
241
        variant: "error",
4✔
242
      });
4✔
243
    } finally {
6✔
244
      setLoading(false);
5✔
245
    }
5✔
246
  };
6✔
247

248
  return (
112✔
249
    <Box>
112✔
250
      <StyledIconButton
112✔
251
        ref={anchorRef}
112✔
252
        onClick={handleToggle}
112✔
253
        aria-label="Export study metadata"
112✔
254
        data-testid="export-study-metadata-toggle"
112✔
255
        {...buttonProps}
112✔
256
      >
257
        <Stack direction="row">
112✔
258
          <StyledDownloadIcon />
112✔
259
          <StyledExpandMoreIcon />
112✔
260
        </Stack>
112✔
261
      </StyledIconButton>
112✔
262
      <Popper open={menuOpen} anchorEl={anchorRef.current} placement="bottom-end">
112✔
263
        <StyledPaper data-testid="export-study-metadata-popper">
112✔
264
          <StyledMenuHeader direction="row" alignItems="center" justifyContent="space-between">
112✔
265
            <StyledMenuTitle variant="subtitle1">Available Downloads</StyledMenuTitle>
112✔
266
            <StyledPopperCloseButton
112✔
267
              aria-label="close"
112✔
268
              data-testid="menu-popper-close-button"
112✔
269
              onClick={handleClose}
112✔
270
            >
271
              <CloseIconSvg />
112✔
272
            </StyledPopperCloseButton>
112✔
273
          </StyledMenuHeader>
112✔
274
          <ClickAwayListener onClickAway={handleClose}>
112✔
275
            <StyledMenuList autoFocusItem={menuOpen}>
112✔
276
              <StyledMenuItem onClick={handleClickSelected} disabled={loading}>
112✔
277
                Download selected metadata
278
                <StyledExportFormat>(TSV)</StyledExportFormat>
112✔
279
              </StyledMenuItem>
112✔
280
              <StyledMenuItem onClick={handleClickFull} disabled={loading}>
112✔
281
                Download full study metadata
282
                <StyledExportFormat>(TSV)</StyledExportFormat>
112✔
283
              </StyledMenuItem>
112✔
284
            </StyledMenuList>
112✔
285
          </ClickAwayListener>
112✔
286
        </StyledPaper>
112✔
287
      </Popper>
112✔
288
    </Box>
112✔
289
  );
290
};
112✔
291

292
export default memo<DataExplorerStudyExportProps>(DataExplorerStudyExport);
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