Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CRDCDH-1206 Submitted Data > Support "Data Files" Viewing #401

Merged
merged 5 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions src/components/DataSubmissions/ExportNodeDataButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe("Basic Functionality", () => {
getSubmissionNodes: {
total: 1,
properties: [],
nodes: [{ nodeType, nodeID: "example-node-id", props: "" }],
nodes: [{ nodeType, nodeID: "example-node-id", props: "", status: null }],
},
},
};
Expand Down Expand Up @@ -239,6 +239,7 @@ describe("Basic Functionality", () => {
{
nodeType: ["aaaa"] as unknown as string,
nodeID: 123 as unknown as string,
status: null,
props: "this is not JSON",
},
],
Expand Down Expand Up @@ -275,7 +276,7 @@ describe("Implementation Requirements", () => {
jest.resetAllMocks();
});

it("should have a tooltip present on the button", async () => {
it("should have a tooltip present on the button for Metadata", async () => {
const { getByTestId, findByRole } = render(
<TestParent mocks={[]}>
<ExportNodeDataButton
Expand All @@ -292,6 +293,52 @@ describe("Implementation Requirements", () => {
expect(tooltip).toHaveTextContent("Export submitted metadata for selected node type");
});

it("should have a tooltip present on the button for Data Files", async () => {
const { getByTestId, findByRole } = render(
<TestParent mocks={[]}>
<ExportNodeDataButton
submission={{ _id: "data-file-tooltip-id", name: "test-tooltip" }}
nodeType="Data File"
/>
</TestParent>
);

UserEvent.hover(getByTestId("export-node-data-button"));

const tooltip = await findByRole("tooltip");
expect(tooltip).toBeInTheDocument();
expect(tooltip).toHaveTextContent("Export a list of all uploaded data files");
});

it("should change the tooltip when the nodeType prop changes", async () => {
const { getByTestId, findByRole, rerender } = render(
<TestParent mocks={[]}>
<ExportNodeDataButton
submission={{ _id: "example-tooltip-id", name: "test-tooltip" }}
nodeType="sample"
/>
</TestParent>
);

UserEvent.hover(getByTestId("export-node-data-button"));

const tooltip = await findByRole("tooltip");
expect(tooltip).toHaveTextContent("Export submitted metadata for selected node type");

rerender(
<TestParent mocks={[]}>
<ExportNodeDataButton
submission={{ _id: "example-tooltip-id", name: "test-tooltip" }}
nodeType="Data File"
/>
</TestParent>
);

UserEvent.hover(getByTestId("export-node-data-button"));

expect(tooltip).toHaveTextContent("Export a list of all uploaded data files");
});

it.each<{ name: string; nodeType: string; date: Date; expected: string }>([
{
name: "Brain",
Expand Down Expand Up @@ -357,7 +404,14 @@ describe("Implementation Requirements", () => {
getSubmissionNodes: {
total: 1,
properties: ["a"],
nodes: [{ nodeType, nodeID: "example-node-id", props: JSON.stringify({ a: 1 }) }],
nodes: [
{
nodeType,
nodeID: "example-node-id",
props: JSON.stringify({ a: 1 }),
status: null,
},
],
},
},
},
Expand Down
21 changes: 14 additions & 7 deletions src/components/DataSubmissions/ExportNodeDataButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { useLazyQuery } from "@apollo/client";
import { IconButtonProps, IconButton, styled } from "@mui/material";
import { CloudDownload } from "@mui/icons-material";
Expand Down Expand Up @@ -53,6 +53,14 @@ export const ExportNodeDataButton: React.FC<Props> = ({
const { enqueueSnackbar } = useSnackbar();
const [loading, setLoading] = useState<boolean>(false);

const tooltip = useMemo<string>(
() =>
nodeType?.toLocaleLowerCase() === "data file"
? "Export a list of all uploaded data files"
: "Export submitted metadata for selected node type",
[nodeType]
);

const [getSubmissionNodes] = useLazyQuery<GetSubmissionNodesResp, GetSubmissionNodesInput>(
GET_SUBMISSION_NODES,
{
Expand Down Expand Up @@ -95,7 +103,10 @@ export const ExportNodeDataButton: React.FC<Props> = ({
try {
const filteredName = filterAlphaNumeric(submission.name?.trim()?.replaceAll(" ", "-"), "-");
const filename = `${filteredName}_${nodeType}_${dayjs().format("YYYYMMDDHHmm")}.tsv`;
const csvArray = d.getSubmissionNodes.nodes.map((node) => JSON.parse(node.props));
const csvArray = d.getSubmissionNodes.nodes.map((node) => ({
...JSON.parse(node.props),
status: node.status,
}));

downloadBlob(unparse(csvArray, { delimiter: "\t" }), filename, "text/tab-separated-values");
} catch (err) {
Expand All @@ -108,11 +119,7 @@ export const ExportNodeDataButton: React.FC<Props> = ({
};

return (
<StyledTooltip
title="Export submitted metadata for selected node type"
placement="top"
data-testid="export-node-data-tooltip"
>
<StyledTooltip title={tooltip} placement="top" data-testid="export-node-data-tooltip">
<span>
<StyledIconButton
onClick={handleClick}
Expand Down
51 changes: 45 additions & 6 deletions src/components/DataSubmissions/SubmittedDataFilters.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,12 @@ describe("SubmittedDataFilters cases", () => {

const muiSelectBox = within(getByTestId("data-content-node-filter")).getByRole("button");

await waitFor(() => expect(muiSelectBox).toHaveTextContent("FIRST"));
await waitFor(() => expect(muiSelectBox).toHaveTextContent(/first/i));
});

it("should NOT show the nodeType 'Data File'", async () => {
// NOTE: This test used to be the inverse, but we now want to ensure that Data Files are shown
// Data Files are a special case, as they're common across all Data Models / Data Commons
it("should show the nodeType 'data file' if present", async () => {
const mocks: MockedResponse<SubmissionStatsResp>[] = [
{
request: {
Expand All @@ -163,7 +165,7 @@ describe("SubmittedDataFilters cases", () => {
stats: [
{ ...baseStatistic, nodeName: "Participant", total: 3 },
{ ...baseStatistic, nodeName: "Data File", total: 2 },
{ ...baseStatistic, nodeName: "File", total: 1 },
{ ...baseStatistic, nodeName: "Sample", total: 1 },
],
},
},
Expand All @@ -184,9 +186,46 @@ describe("SubmittedDataFilters cases", () => {
await waitFor(() => {
// Sanity check that the box is open
expect(() => getAllByText(/participant/i)).not.toThrow();
expect(() => getByText(/file/i)).not.toThrow();
// This should throw an error
expect(() => getByText(/data file/i)).toThrow();
expect(() => getByText(/sample/i)).not.toThrow();
expect(() => getByText(/data file/i)).not.toThrow();
});
});

it("should visually render the nodeName as lowercase", async () => {
const mocks: MockedResponse<SubmissionStatsResp>[] = [
{
request: {
query: SUBMISSION_STATS,
},
variableMatcher: () => true,
result: {
data: {
submissionStats: {
stats: [
{ ...baseStatistic, nodeName: "NODE_NAME", total: 1 },
{ ...baseStatistic, nodeName: "Upper_Case", total: 1 },
{ ...baseStatistic, nodeName: "lower_case", total: 1 },
],
},
},
},
},
];

const { getByTestId } = render(
<TestParent mocks={mocks}>
<SubmittedDataFilters submissionId="id-test-filtering-lower-case" />
</TestParent>
);

const muiSelectBox = within(getByTestId("data-content-node-filter")).getByRole("button");

UserEvent.click(muiSelectBox);

await waitFor(() => {
expect(getByTestId("nodeType-NODE_NAME")).toHaveTextContent("node_name");
expect(getByTestId("nodeType-Upper_Case")).toHaveTextContent("upper_case");
expect(getByTestId("nodeType-lower_case")).toHaveTextContent("lower_case");
});
});

Expand Down
5 changes: 2 additions & 3 deletions src/components/DataSubmissions/SubmittedDataFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ export const SubmittedDataFilters: FC<SubmittedDataFiltersProps> = ({
cloneDeep(data?.submissionStats?.stats)
?.sort(compareNodeStats)
?.reverse()
?.map((stat) => stat.nodeName)
?.filter((nodeType) => nodeType?.toLowerCase() !== "data file"),
?.map((stat) => stat.nodeName),
[data?.submissionStats?.stats]
);

Expand Down Expand Up @@ -94,7 +93,7 @@ export const SubmittedDataFilters: FC<SubmittedDataFiltersProps> = ({
>
{nodeTypes?.map((nodeType) => (
<MenuItem key={nodeType} value={nodeType} data-testid={`nodeType-${nodeType}`}>
{nodeType}
{nodeType.toLowerCase()}
</MenuItem>
))}
</StyledSelect>
Expand Down
60 changes: 60 additions & 0 deletions src/content/dataSubmissions/SubmittedData.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ describe("SubmittedData > Table", () => {
"col.2": "value-2",
"col.3": "value-3",
}),
status: "New",
},
],
},
Expand All @@ -262,6 +263,65 @@ describe("SubmittedData > Table", () => {
});
});

it("should append the 'Status' column to any node type", async () => {
const submissionID = "example-status-column-id";

const mocks: MockedResponse[] = [
mockSubmissionQuery,
{
request: {
query: GET_SUBMISSION_NODES,
variables: {
_id: submissionID,
sortDirection: "desc",
first: 20,
offset: 0,
nodeType: "example-node",
},
},
result: {
data: {
getSubmissionNodes: {
total: 2,
properties: ["col-xyz"],
nodes: [
{
nodeType: "example-node",
nodeID: "example-node-id",
props: JSON.stringify({
"col-xyz": "value-1",
}),
status: "New",
},
{
nodeType: "example-node2",
nodeID: "example-node-id2",
props: JSON.stringify({
"col-xyz": "value-2",
}),
status: null,
},
],
},
},
},
},
];

const { getByTestId, getByText } = render(
<TestParent mocks={mocks}>
<SubmittedData submissionId={submissionID} submissionName={undefined} />
</TestParent>
);

await waitFor(() => {
expect(getByTestId("generic-table-header-col-xyz")).toBeInTheDocument();
expect(getByTestId("generic-table-header-Status")).toBeInTheDocument();
expect(getByText("value-1")).toBeInTheDocument();
expect(getByText("New")).toBeInTheDocument();
});
});

// NOTE: We're asserting that the columns ARE built using getSubmissionNodes.properties
// instead of the keys of nodes.[x].props JSON object
it("should NOT build the columns based off of the nodes.[X].props JSON object", async () => {
Expand Down
17 changes: 12 additions & 5 deletions src/content/dataSubmissions/SubmittedData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { safeParse } from "../../utils";
import { ExportNodeDataButton } from "../../components/DataSubmissions/ExportNodeDataButton";

type T = Pick<SubmissionNode, "nodeType" | "nodeID"> & {
type T = Pick<SubmissionNode, "nodeType" | "nodeID" | "status"> & {
props: Record<string, string>;
};

Expand Down Expand Up @@ -97,16 +97,22 @@ const SubmittedData: FC<Props> = ({ submissionId, submissionName }) => {

// Only update columns if the nodeType has changed
if (prevFilterRef.current.nodeType !== filterRef.current.nodeType) {
setTotalData(d.getSubmissionNodes.total);
setColumns(
d.getSubmissionNodes.properties.map((prop: string, index: number) => ({
const cols: Column<T>[] = d.getSubmissionNodes.properties.map(
(prop: string, index: number) => ({
label: prop,
renderValue: (d) => d?.props?.[prop] || "",
// NOTE: prop is not actually a keyof T, but it's a value of prop.props
field: prop as unknown as keyof T,
default: index === 0 ? true : undefined,
}))
})
);
cols.push({
label: "Status",
renderValue: (d) => d?.status || "",
field: "status",
});
setTotalData(d.getSubmissionNodes.total);
setColumns(cols);

prevFilterRef.current = filterRef.current;
}
Expand All @@ -116,6 +122,7 @@ const SubmittedData: FC<Props> = ({ submissionId, submissionName }) => {
nodeType: node.nodeType,
nodeID: node.nodeID,
props: safeParse(node.props),
status: node.status,
}))
);
setLoading(false);
Expand Down
3 changes: 2 additions & 1 deletion src/graphql/getSubmissionNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const query = gql`
nodes {
nodeType
nodeID
status
props
}
}
Expand Down Expand Up @@ -52,6 +53,6 @@ export type Response = {
*
* @note Unused values are omitted from the query. See the type definition for additional fields.
*/
nodes: Pick<SubmissionNode, "nodeType" | "nodeID" | "props">[];
nodes: Pick<SubmissionNode, "nodeType" | "nodeID" | "props" | "status">[];
};
};
2 changes: 1 addition & 1 deletion src/types/Submissions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ type SubmissionNode = {
submissionID: string;
nodeType: string;
nodeID: string;
status: string;
status: ValidationStatus;
createdAt: string;
updatedAt: string;
validatedAt: string;
Expand Down
Loading