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

CBIIT / crdc-datahub-ui / 16329552935

16 Jul 2025 08:24PM UTC coverage: 71.571% (+0.1%) from 71.457%
16329552935

push

github

web-flow
Merge pull request #761 from CBIIT/CRDCDH-2897

CRDCDH-2897 Data Explorer Column Toggle

3713 of 4098 branches covered (90.61%)

Branch coverage included in aggregate %.

221 of 246 new or added lines in 8 files covered. (89.84%)

2 existing lines in 1 file now uncovered.

23449 of 33853 relevant lines covered (69.27%)

109.38 hits per line

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

78.29
/src/content/DataExplorer/StudyView.tsx
1
import { useLazyQuery, useQuery } from "@apollo/client";
1✔
2
import { Box, styled } from "@mui/material";
1✔
3
import { cloneDeep } from "lodash";
1✔
4
import React, { FC, memo, useCallback, useMemo, useRef, useState } from "react";
1✔
5
import { Navigate, useNavigate } from "react-router-dom";
1✔
6

7
import bannerPng from "../../assets/banner/submission_banner.png";
1✔
8
import { useSearchParamsContext } from "../../components/Contexts/SearchParamsContext";
1✔
9
import DataExplorerExportButton from "../../components/DataExplorerExportButton";
1✔
10
import DataExplorerFilters, { FilterForm } from "../../components/DataExplorerFilters";
1✔
11
import GenericTable, { Column } from "../../components/GenericTable";
1✔
12
import ColumnVisibilityButton from "../../components/GenericTable/ColumnVisibilityButton";
1✔
13
import { ColumnVisibilityPopperGroup } from "../../components/GenericTable/ColumnVisibilityPopper";
14
import NavigationBreadcrumbs, { BreadcrumbEntry } from "../../components/NavigationBreadcrumbs";
1✔
15
import PageContainer from "../../components/PageContainer";
1✔
16
import SuspenseLoader from "../../components/SuspenseLoader";
1✔
17
import TruncatedText from "../../components/TruncatedText";
1✔
18
import {
1✔
19
  GET_APPROVED_STUDY,
20
  GET_RELEASED_NODE_TYPES,
21
  GetApprovedStudyInput,
22
  GetApprovedStudyResp,
23
  GetReleasedNodeTypesInput,
24
  GetReleasedNodeTypesResp,
25
  LIST_RELEASED_DATA_RECORDS,
26
  ListReleasedDataRecordsInput,
27
  ListReleasedDataRecordsResponse,
28
  RETRIEVE_PROPS_FOR_NODE_TYPE,
29
  RetrievePropsForNodeTypeInput,
30
  RetrievePropsForNodeTypeResp,
31
} from "../../graphql";
32
import { useColumnVisibility } from "../../hooks/useColumnVisibility";
1✔
33
import usePageTitle from "../../hooks/usePageTitle";
1✔
34
import { coerceToString, Logger } from "../../utils";
1✔
35

36
const StyledBreadcrumbsBox = styled(Box)({
1✔
37
  height: "50px",
1✔
38
  display: "flex",
1✔
39
  alignItems: "center",
1✔
40
  padding: "0 54px",
1✔
41
});
1✔
42

43
const StyledFilterTableWrapper = styled(Box)({
1✔
44
  borderRadius: "8px",
1✔
45
  background: "#FFF",
1✔
46
  border: "1px solid #6CACDA",
1✔
47
  marginBottom: "25px",
1✔
48
});
1✔
49

50
type T = ListReleasedDataRecordsResponse["listReleasedDataRecords"]["nodes"][number];
51

52
type StudyViewProps = {
53
  _id: string;
54
};
55

56
const StudyView: FC<StudyViewProps> = ({ _id: studyId }) => {
1✔
57
  usePageTitle(`Data Explorer - ${studyId}`);
58✔
58

59
  const navigate = useNavigate();
58✔
60
  const { searchParams, lastSearchParams } = useSearchParamsContext();
58✔
61

62
  const [loading, setLoading] = useState<boolean>(false);
58✔
63
  const [data, setData] = useState<T[]>([]);
58✔
64
  const [totalData, setTotalData] = useState<number>(0);
58✔
65

66
  const tableRef = useRef<TableMethods>(null);
58✔
67
  const filtersRef = useRef<FilterForm>();
58✔
68
  const prevFiltersRef = useRef<FilterForm>(null);
58✔
69

70
  const dataCommonsDisplayName = searchParams.get("dataCommonsDisplayName");
58✔
71
  const returnUrl = `/data-explorer${lastSearchParams?.["/data-explorer"] ?? ""}`;
58✔
72

73
  const { data: studyData, loading: studyLoading } = useQuery<
58✔
74
    GetApprovedStudyResp<true>,
75
    GetApprovedStudyInput
76
  >(GET_APPROVED_STUDY, {
58✔
77
    variables: { _id: studyId, partial: true },
58✔
78
    skip: !studyId || !dataCommonsDisplayName,
58✔
79
    fetchPolicy: "cache-first",
58✔
80
    context: { clientName: "backend" },
58✔
81
    onError: (error) => {
58✔
82
      Logger.error("Error fetching study data:", error);
2✔
83
    },
2✔
84
  });
58✔
85

86
  const { data: nodesData, loading: nodesLoading } = useQuery<
58✔
87
    GetReleasedNodeTypesResp,
88
    GetReleasedNodeTypesInput
89
  >(GET_RELEASED_NODE_TYPES, {
58✔
90
    variables: { studyId, dataCommonsDisplayName },
58✔
91
    skip: !studyId || !dataCommonsDisplayName,
58✔
92
    fetchPolicy: "cache-first",
58✔
93
    context: { clientName: "backend" },
58✔
94
    onError: (error) => {
58✔
95
      Logger.error("Error fetching node list:", error);
3✔
96
    },
3✔
97
  });
58✔
98

99
  const { data: nodeProps, loading: nodePropsLoading } = useQuery<
58✔
100
    RetrievePropsForNodeTypeResp,
101
    RetrievePropsForNodeTypeInput
102
  >(RETRIEVE_PROPS_FOR_NODE_TYPE, {
58✔
103
    variables: { studyId, dataCommonsDisplayName, nodeType: filtersRef.current?.nodeType },
58!
104
    skip: !studyId || !dataCommonsDisplayName || !filtersRef.current?.nodeType,
58!
105
    fetchPolicy: "cache-first",
58✔
106
    context: { clientName: "backend" },
58✔
107
    onError: (error) => {
58✔
NEW
108
      Logger.error("Error fetching node properties:", error);
×
NEW
109
      navigate("/data-explorer", {
×
NEW
110
        state: {
×
NEW
111
          alert: true,
×
NEW
112
          error: "Oops! Unable to display metadata for the selected study or data commons.",
×
NEW
113
        },
×
NEW
114
      });
×
NEW
115
    },
×
116
  });
58✔
117

118
  const [listReleasedDataRecords] = useLazyQuery<
58✔
119
    ListReleasedDataRecordsResponse,
120
    ListReleasedDataRecordsInput
121
  >(LIST_RELEASED_DATA_RECORDS, {
58✔
122
    context: { clientName: "backend" },
58✔
123
    fetchPolicy: "cache-and-network",
58✔
124
  });
58✔
125

126
  const columnVisibilityKey = useMemo<string>(
58✔
127
    () =>
58✔
128
      `dataExplorerColumns:${studyId}:${dataCommonsDisplayName}:${filtersRef.current?.nodeType}`,
17!
129
    [studyId, dataCommonsDisplayName, filtersRef.current?.nodeType]
58!
130
  );
58✔
131

132
  const selectedNodeType = useMemo<
58✔
133
    GetReleasedNodeTypesResp["getReleaseNodeTypes"]["nodes"][number] | null
134
  >(
135
    () =>
58✔
136
      nodesData?.getReleaseNodeTypes?.nodes?.find(
27✔
137
        (node) => node.name === filtersRef.current?.nodeType
27!
138
      ) || null,
27✔
139
    [nodesData, filtersRef.current?.nodeType]
58!
140
  );
58✔
141

142
  const nodeTypeOptions = useMemo<string[]>(() => {
58✔
143
    if (!nodesData || !nodesData?.getReleaseNodeTypes?.nodes.length) {
27✔
144
      return [];
19✔
145
    }
19✔
146

147
    const clonedData = cloneDeep(nodesData.getReleaseNodeTypes.nodes).sort((a, b) => {
8✔
148
      if (a.count === b.count) {
4!
149
        return a.name.localeCompare(b.name);
×
150
      }
×
151

152
      return a.count - b.count;
4✔
153
    });
8✔
154

155
    return clonedData.map((node) => node.name);
8✔
156
  }, [nodesData]);
58✔
157

158
  const defaultValues = useMemo<FilterForm>(
58✔
159
    () => ({
58✔
160
      nodeType: nodeTypeOptions?.[0] || "",
27✔
161
    }),
27✔
162
    [nodeTypeOptions]
58✔
163
  );
58✔
164

165
  const studyDisplayName = useMemo<string>(
58✔
166
    () =>
58✔
167
      studyData?.getApprovedStudy?.studyAbbreviation ||
28✔
168
      studyData?.getApprovedStudy?.studyName ||
27✔
169
      "",
25✔
170
    [studyData]
58✔
171
  );
58✔
172

173
  const breadcrumbs = useMemo<BreadcrumbEntry[]>(
58✔
174
    () => [
58✔
175
      {
20✔
176
        label: "Data Explorer",
20✔
177
        to: returnUrl,
20✔
178
      },
20✔
179
      {
20✔
180
        label: studyDisplayName,
20✔
181
      },
20✔
182
    ],
183
    [studyDisplayName]
58✔
184
  );
58✔
185

186
  const columns = useMemo<Column<T>[]>(
58✔
187
    () =>
58✔
188
      nodeProps?.retrievePropsForNodeType?.map((prop) => ({
17!
NEW
189
        label: prop.name,
×
190
        renderValue: (d: T) => (
×
191
          <TruncatedText
×
NEW
192
            text={coerceToString(d[prop.name])}
×
193
            maxCharacters={20}
×
194
            disableInteractiveTooltip={false}
×
195
            arrow
×
196
            ellipsis
×
197
            underline
×
198
          />
199
        ),
NEW
200
        field: prop.name,
×
NEW
201
        default: prop.name === selectedNodeType?.IDPropName ? true : undefined,
×
NEW
202
        hideable: prop.name !== selectedNodeType?.IDPropName,
×
NEW
203
        defaultHidden: prop.required !== true,
×
204
      })) || [],
17✔
205
    [selectedNodeType?.IDPropName, nodeProps?.retrievePropsForNodeType]
58!
206
  );
58✔
207

208
  const { visibleColumns, columnVisibilityModel, setColumnVisibilityModel } = useColumnVisibility<
58✔
209
    Column<T>
210
  >({
58✔
211
    columns,
58✔
212
    getColumnKey: (c) => c.field,
58✔
213
    localStorageKey: columnVisibilityKey,
58✔
214
  });
58✔
215

216
  const handleFetchData = useCallback(
58✔
217
    async (params: FetchListing<T>, force: boolean) => {
58✔
218
      const { offset, first } = params;
×
219

220
      // NOTE: This is a workaround to stabilize the number of API calls when switching between node types
221
      // It will override the previous orderBy if the node type changes
222
      if (prevFiltersRef.current?.nodeType !== filtersRef.current?.nodeType) {
×
223
        params.orderBy = nodesData?.getReleaseNodeTypes?.nodes?.find(
×
224
          (node) => node.name === filtersRef.current?.nodeType
×
225
        )?.IDPropName;
×
226
        params.sortDirection = "asc";
×
227
      }
×
228

229
      setLoading(true);
×
230
      const { data, error } = await listReleasedDataRecords({
×
231
        variables: {
×
232
          studyId,
×
233
          dataCommonsDisplayName,
×
234
          nodeType: filtersRef.current?.nodeType,
×
235
          orderBy: params.orderBy,
×
236
          sortDirection: params.sortDirection,
×
237
          offset,
×
238
          first,
×
239
        },
×
240
      });
×
241

242
      if (error) {
×
243
        Logger.error(
×
244
          "Error occurred while fetching released records for node",
×
245
          filtersRef?.current?.nodeType,
×
246
          error
×
247
        );
×
248
        setLoading(false);
×
249
        return;
×
250
      }
×
251

252
      setData(data?.listReleasedDataRecords?.nodes || []);
×
253
      setTotalData(data?.listReleasedDataRecords?.total || 0);
×
254
      prevFiltersRef.current = cloneDeep(filtersRef.current);
×
UNCOV
255
      setLoading(false);
×
UNCOV
256
    },
×
257
    [
58✔
258
      studyId,
58✔
259
      nodesData?.getReleaseNodeTypes?.nodes,
58✔
260
      dataCommonsDisplayName,
58✔
261
      filtersRef.current,
58✔
262
      prevFiltersRef.current,
58✔
263
      listReleasedDataRecords,
58✔
264
    ]
265
  );
58✔
266

267
  const handleFilterChange = useCallback(
58✔
268
    (data: FilterForm) => {
58✔
269
      filtersRef.current = data;
×
270
      tableRef.current?.setPage(0, true);
×
271
    },
×
272
    [filtersRef.current, tableRef.current]
58✔
273
  );
58✔
274

275
  const handleSetItemKey = useCallback(
58✔
276
    (d: T, idx: number) => `data-${d?.[selectedNodeType?.IDPropName] || idx}`,
58✔
277
    [columns, selectedNodeType?.IDPropName]
58!
278
  );
58✔
279

280
  const handleGetColumnGroup = useCallback(
58✔
281
    (column: Column<T>) => {
58✔
NEW
282
      const prop = nodeProps?.retrievePropsForNodeType.find((p) => p.name === column.field);
×
NEW
283
      switch (prop?.group) {
×
NEW
284
        case "not_defined":
×
NEW
285
          return "Others";
×
NEW
286
        case "internal":
×
NEW
287
          return "Internal";
×
NEW
288
        case "model_defined":
×
NEW
289
        default:
×
NEW
290
          return "Data Model Defined";
×
NEW
291
      }
×
NEW
292
    },
×
293
    [nodeProps?.retrievePropsForNodeType]
58!
294
  );
58✔
295

296
  const handleGetColumnKey = useCallback((column: Column<T>) => column.field, []);
58✔
297

298
  const handleGetColumnLabel = useCallback((column: Column<T>) => column.label?.toString(), []);
58✔
299

300
  const columnGroups = useMemo<ColumnVisibilityPopperGroup[]>(
58✔
301
    () => [
58✔
302
      {
17✔
303
        name: "Data Model Defined",
17✔
304
        description: "Fields defined in the data model and submitted by users.",
17✔
305
      },
17✔
306
      {
17✔
307
        name: "Others",
17✔
308
        description: "Additional fields not included in the data model and submitted by users.",
17✔
309
      },
17✔
310
      {
17✔
311
        name: "Internal",
17✔
312
        description: "Fields automatically created by the system.",
17✔
313
      },
17✔
314
    ],
315
    []
58✔
316
  );
58✔
317

318
  const filterActions = useMemo<React.ReactNode[]>(
58✔
319
    () => [
58✔
320
      <ColumnVisibilityButton
58✔
321
        key="column-visibility-action"
58✔
322
        columns={columns}
58✔
323
        groups={columnGroups}
58✔
324
        getColumnKey={handleGetColumnKey}
58✔
325
        getColumnLabel={handleGetColumnLabel}
58✔
326
        getColumnGroup={handleGetColumnGroup}
58✔
327
        columnVisibilityModel={columnVisibilityModel}
58✔
328
        onColumnVisibilityModelChange={setColumnVisibilityModel}
58✔
329
        data-testid="column-visibility-button"
58✔
330
      />,
331
      <DataExplorerExportButton
58✔
332
        key="export-data-action"
58✔
333
        studyId={studyId}
58✔
334
        studyDisplayName={studyDisplayName}
58✔
335
        nodeType={filtersRef.current?.nodeType || ""}
58!
336
        dataCommonsDisplayName={dataCommonsDisplayName || ""}
58✔
337
        columns={visibleColumns}
58✔
338
      />,
58✔
339
    ],
340
    [
58✔
341
      studyId,
58✔
342
      columns,
58✔
343
      columnGroups,
58✔
344
      columnVisibilityModel,
58✔
345
      filtersRef?.current?.nodeType,
58!
346
      dataCommonsDisplayName,
58✔
347
      setColumnVisibilityModel,
58✔
348
      handleGetColumnGroup,
58✔
349
      handleGetColumnKey,
58✔
350
      handleGetColumnLabel,
58✔
351
    ]
352
  );
58✔
353

354
  if (studyLoading || nodesLoading) {
58✔
355
    return <SuspenseLoader fullscreen data-testid="study-view-loader" />;
44✔
356
  }
44✔
357

358
  if (!dataCommonsDisplayName || !studyData?.getApprovedStudy?._id || !nodeTypeOptions?.length) {
58✔
359
    return (
8✔
360
      <Navigate
8✔
361
        to="/data-explorer"
8✔
362
        state={{
8✔
363
          alert: true,
8✔
364
          error: "Oops! Unable to display metadata for the selected study or data commons.",
8✔
365
        }}
8✔
366
      />
367
    );
368
  }
8✔
369

370
  return (
6✔
371
    <Box data-testid="study-view-container">
6✔
372
      {/* Page Breadcrumbs */}
373
      <StyledBreadcrumbsBox>
6✔
374
        <NavigationBreadcrumbs entries={breadcrumbs} />
6✔
375
      </StyledBreadcrumbsBox>
6✔
376

377
      {/* Page Header, Filters, & Table */}
378
      <PageContainer
6✔
379
        background={bannerPng}
6✔
380
        title="Data Explorer for Study - "
6✔
381
        titleSuffix={studyDisplayName}
6✔
382
        description="Select a node type to view metadata associated with the selected study. The table below displays all available metadata for the chosen node type."
6✔
383
      >
384
        <StyledFilterTableWrapper>
6✔
385
          <DataExplorerFilters
6✔
386
            nodeTypes={nodeTypeOptions}
6✔
387
            defaultValues={defaultValues}
6✔
388
            onChange={handleFilterChange}
6✔
389
            actions={filterActions}
6✔
390
          />
391

392
          <GenericTable
6✔
393
            ref={tableRef}
6✔
394
            columns={visibleColumns}
6✔
395
            data={data}
6✔
396
            total={totalData}
6✔
397
            loading={loading || nodePropsLoading}
6✔
398
            delayedLoadingTimeMs={0}
58✔
399
            defaultRowsPerPage={20}
58✔
400
            defaultOrder="asc"
58✔
401
            disableUrlParams
58✔
402
            position="bottom"
58✔
403
            onFetchData={handleFetchData}
58✔
404
            setItemKey={handleSetItemKey}
58✔
405
            containerProps={{
58✔
406
              sx: {
58✔
407
                minHeight: "175px",
58✔
408
                marginBottom: "8px",
58✔
409
                border: 0,
58✔
410
                borderTopLeftRadius: 0,
58✔
411
                borderTopRightRadius: 0,
58✔
412
              },
58✔
413
            }}
58✔
414
          />
415
        </StyledFilterTableWrapper>
58✔
416
      </PageContainer>
58✔
417
    </Box>
58✔
418
  );
419
};
58✔
420

421
export default memo<StudyViewProps>(StudyView);
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