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

CBIIT / crdc-datahub-ui / 9714276761

27 Jun 2024 06:56PM UTC coverage: 34.674% (+0.8%) from 33.898%
9714276761

push

github

web-flow
Merge pull request #403 from CBIIT/CRDCDH-1155

CRDCDH-1155 Data Submission Validation Status

1193 of 4032 branches covered (29.59%)

Branch coverage included in aggregate %.

82 of 84 new or added lines in 8 files covered. (97.62%)

4 existing lines in 3 files now uncovered.

1868 of 4796 relevant lines covered (38.95%)

94.71 hits per line

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

91.45
/src/components/DataSubmissions/ValidationControls.tsx
1
import React, { FC, ReactElement, useEffect, useMemo, useRef, useState } from "react";
2
import { useMutation } from "@apollo/client";
3
import { FormControlLabel, RadioGroup, Stack, Typography, styled } from "@mui/material";
4
import { LoadingButton } from "@mui/lab";
5
import { useSnackbar } from "notistack";
6
import { useAuthContext } from "../Contexts/AuthContext";
7
import StyledRadioButton from "../Questionnaire/StyledRadioButton";
8
import {
9
  VALIDATE_SUBMISSION,
10
  ValidateSubmissionInput,
11
  ValidateSubmissionResp,
12
} from "../../graphql";
13
import {
14
  getDefaultValidationTarget,
15
  getDefaultValidationType,
16
  getValidationTypes,
17
} from "../../utils";
18
import FlowWrapper from "./FlowWrapper";
19
import { CrossValidationButton } from "./CrossValidationButton";
20
import { ValidationStatus } from "./ValidationStatus";
21
import { useSubmissionContext } from "../Contexts/SubmissionContext";
22

23
const StyledValidateButton = styled(LoadingButton)({
2✔
24
  padding: "10px",
25
  fontSize: "16px",
26
  fontStyle: "normal",
27
  lineHeight: "24px",
28
  letterSpacing: "0.32px",
29
  "&.MuiButtonBase-root": {
30
    marginLeft: "auto",
31
    minWidth: "137px",
32
  },
33
});
34

35
const StyledRow = styled(Stack)({
2✔
36
  fontFamily: "Nunito",
37
});
38

39
const StyledRowTitle = styled(Typography)({
2✔
40
  fontWeight: 700,
41
  fontSize: "16px",
42
  color: "#083A50",
43
  minWidth: "170px",
44
});
45

46
const StyledRowContent = styled(Stack)({
2✔
47
  display: "flex",
48
  flexDirection: "row",
49
  alignItems: "center",
50
  width: "650px",
51
});
52

53
const StyledRadioControl = styled(FormControlLabel)({
2✔
54
  fontFamily: "Nunito",
55
  fontSize: "16px",
56
  fontWeight: "500",
57
  lineHeight: "20px",
58
  textAlign: "left",
59
  color: "#083A50",
60
  minWidth: "230px",
61
  "&:last-child": {
62
    marginRight: "0px",
63
    minWidth: "unset",
64
  },
65
});
66

67
/**
68
 * Base set of user roles that can validate a submission.
69
 */
70
const BaseValidateRoles: User["role"][] = [
2✔
71
  "Submitter",
72
  "Data Curator",
73
  "Organization Owner",
74
  "Admin",
75
];
76

77
/**
78
 * A map from Submission Status to the user roles that can validate the submission for that status.
79
 *
80
 * @note All of the permission logic really should be refactored into a hook or otherwise.
81
 */
82
const ValidateMap: Partial<Record<Submission["status"], User["role"][]>> = {
2✔
83
  "In Progress": BaseValidateRoles,
84
  Withdrawn: BaseValidateRoles,
85
  Rejected: BaseValidateRoles,
86
  Submitted: ["Data Curator", "Admin"],
87
};
88

89
/**
90
 * Provides the UI for validating a data submission's assets.
91
 *
92
 * @returns {React.FC}
93
 */
94
const ValidationControls: FC = () => {
2✔
95
  const { user } = useAuthContext();
250✔
96
  const { enqueueSnackbar } = useSnackbar();
250✔
97
  const { data, updateQuery, refetch } = useSubmissionContext();
250✔
98
  const { getSubmission: dataSubmission } = data || {};
250!
99

100
  const [validationType, setValidationType] = useState<ValidationType | "All">(null);
250✔
101
  const [uploadType, setUploadType] = useState<ValidationTarget>(null);
250✔
102
  const [isLoading, setIsLoading] = useState<boolean>(false);
250✔
103

104
  const isValidating = useMemo<boolean>(
250✔
105
    () =>
106
      dataSubmission?.fileValidationStatus === "Validating" ||
100✔
107
      dataSubmission?.metadataValidationStatus === "Validating",
108
    [dataSubmission?.fileValidationStatus, dataSubmission?.metadataValidationStatus]
109
  );
110
  const prevIsValidating = useRef<boolean>(isValidating);
250✔
111

112
  const canValidateMetadata: boolean = useMemo(() => {
250✔
113
    const permissionMap = ValidateMap[dataSubmission?.status];
100✔
114
    if (!user?.role || !dataSubmission?.status || !permissionMap) {
100✔
115
      return false;
18✔
116
    }
117
    if (permissionMap.includes(user.role) === false) {
82✔
118
      return false;
20✔
119
    }
120

121
    return dataSubmission?.metadataValidationStatus !== null;
62✔
122
  }, [user?.role, dataSubmission?.metadataValidationStatus, dataSubmission?.status]);
123

124
  const canValidateFiles: boolean = useMemo(() => {
250✔
125
    const permissionMap = ValidateMap[dataSubmission?.status];
100✔
126
    if (!user?.role || !dataSubmission?.status || !permissionMap) {
100✔
127
      return false;
18✔
128
    }
129
    if (permissionMap.includes(user.role) === false) {
82✔
130
      return false;
20✔
131
    }
132
    if (dataSubmission.intention === "Delete" || dataSubmission.dataType === "Metadata Only") {
62✔
133
      return false;
4✔
134
    }
135

136
    return dataSubmission?.fileValidationStatus !== null;
58✔
137
  }, [user?.role, dataSubmission?.fileValidationStatus, dataSubmission?.status]);
138

139
  const [validateSubmission] = useMutation<ValidateSubmissionResp, ValidateSubmissionInput>(
250✔
140
    VALIDATE_SUBMISSION,
141
    {
142
      context: { clientName: "backend" },
143
      fetchPolicy: "no-cache",
144
    }
145
  );
146

147
  const handleValidateFiles = async () => {
250✔
148
    if (isValidating || !validationType || !uploadType) {
22!
149
      return;
×
150
    }
151
    if (!canValidateFiles && validationType === "file") {
22!
152
      return;
×
153
    }
154
    if (!canValidateMetadata && validationType === "metadata") {
22!
155
      return;
×
156
    }
157

158
    setIsLoading(true);
22✔
159

160
    const { data, errors } = await validateSubmission({
22✔
161
      variables: {
162
        _id: dataSubmission?._id,
163
        types: getValidationTypes(validationType),
164
        scope: uploadType,
165
      },
166
    }).catch((e) => ({ errors: e?.message, data: null }));
4✔
167

168
    if (errors || !data?.validateSubmission?.success) {
22✔
169
      enqueueSnackbar("Unable to initiate validation process.", {
4✔
170
        variant: "error",
171
      });
172
    } else {
173
      enqueueSnackbar(
18✔
174
        "Validation process is starting; this may take some time. Please wait before initiating another validation.",
175
        { variant: "success" }
176
      );
177
      handleOnValidate();
18✔
178
    }
179

180
    setIsLoading(false);
22✔
181
  };
182

183
  const handleOnValidate = () => {
250✔
184
    // NOTE: This forces the UI to rerender with the new statuses immediately
185
    const types = getValidationTypes(validationType);
18✔
186
    updateQuery((prev) => ({
18✔
187
      ...prev,
188
      getSubmission: {
189
        ...prev.getSubmission,
190
        fileValidationStatus: types?.includes("file")
×
191
          ? "Validating"
192
          : prev?.getSubmission?.fileValidationStatus,
193
        metadataValidationStatus: types?.includes("metadata")
×
194
          ? "Validating"
195
          : prev?.getSubmission?.metadataValidationStatus,
196
        validationStarted: new Date().toISOString(),
197
        validationEnded: null,
198
        validationType: types,
199
        validationScope: uploadType,
200
      },
201
    }));
202

203
    // Kick off polling to check for validation status change
204
    // NOTE: We're waiting 1000ms to allow the cache to update
205
    setTimeout(refetch, 1000);
18✔
206
  };
207

208
  const Actions: ReactElement = useMemo(
250✔
209
    () => (
210
      <>
250✔
211
        <StyledValidateButton
212
          variant="contained"
213
          color="info"
214
          disabled={(!canValidateFiles && !canValidateMetadata) || isValidating}
275✔
215
          loading={isLoading}
216
          onClick={handleValidateFiles}
217
          data-testid="validate-controls-validate-button"
218
        >
219
          {isValidating ? "Validating..." : "Validate"}
125✔
220
        </StyledValidateButton>
221
        <CrossValidationButton submission={dataSubmission} variant="contained" color="info" />
222
      </>
223
    ),
224
    [
225
      handleValidateFiles,
226
      dataSubmission,
227
      canValidateFiles,
228
      canValidateMetadata,
229
      isValidating,
230
      isLoading,
231
    ]
232
  );
233

234
  useEffect(() => {
250✔
235
    const isValidating =
236
      dataSubmission?.fileValidationStatus === "Validating" ||
100✔
237
      dataSubmission?.metadataValidationStatus === "Validating";
238

239
    // Reset the validation type and target only if the validation process finished
240
    if (!isValidating && prevIsValidating.current === true) {
100✔
241
      setValidationType(getDefaultValidationType(dataSubmission, user, ValidateMap));
4✔
242
      setUploadType(getDefaultValidationTarget(dataSubmission, user, ValidateMap));
4✔
243
    }
244

245
    prevIsValidating.current = isValidating;
100✔
246
  }, [dataSubmission?.fileValidationStatus, dataSubmission?.metadataValidationStatus]);
247

248
  useEffect(() => {
250✔
249
    if (typeof dataSubmission === "undefined") {
100!
UNCOV
250
      return;
×
251
    }
252
    if (validationType === null) {
100✔
253
      setValidationType(getDefaultValidationType(dataSubmission, user, ValidateMap));
90✔
254
    }
255
    if (uploadType === null) {
100✔
256
      setUploadType(getDefaultValidationTarget(dataSubmission, user, ValidateMap));
90✔
257
    }
258
  }, [dataSubmission, user]);
259

260
  return (
250✔
261
    <FlowWrapper
262
      index={3}
263
      titleContainerSx={{ marginBottom: "4px", columnGap: "12px" }}
264
      title="Validate Data"
265
      titleAdornment={<ValidationStatus />}
266
      actions={Actions}
267
      last
268
    >
269
      <>
270
        <StyledRow direction="row" alignItems="center" sx={{ marginBottom: "-5px" }}>
271
          <StyledRowTitle>Validation Type:</StyledRowTitle>
272
          <StyledRowContent>
273
            <RadioGroup
274
              value={validationType}
275
              onChange={(e, val: ValidationType) => setValidationType(val)}
6✔
276
              data-testid="validate-controls-validation-type"
277
              row
278
            >
279
              <StyledRadioControl
280
                value="metadata"
281
                control={<StyledRadioButton readOnly={false} />}
282
                label="Validate Metadata"
283
                disabled={!canValidateMetadata}
284
              />
285
              <StyledRadioControl
286
                value="file"
287
                control={<StyledRadioButton readOnly={false} />}
288
                label="Validate Data Files"
289
                disabled={!canValidateFiles}
290
              />
291
              <StyledRadioControl
292
                value="All"
293
                control={<StyledRadioButton readOnly={false} />}
294
                label="Both"
295
                disabled={!canValidateFiles || !canValidateMetadata}
185✔
296
              />
297
            </RadioGroup>
298
          </StyledRowContent>
299
        </StyledRow>
300
        <StyledRow direction="row" alignItems="center" sx={{ marginTop: "-5px" }}>
301
          <StyledRowTitle>Validation Target:</StyledRowTitle>
302
          <StyledRowContent>
303
            <RadioGroup
304
              value={uploadType}
305
              onChange={(event, val: ValidationTarget) => setUploadType(val)}
6✔
306
              data-testid="validate-controls-validation-target"
307
              row
308
            >
309
              <StyledRadioControl
310
                value="New"
311
                control={<StyledRadioButton readOnly={false} />}
312
                label="New Uploaded Data"
313
                disabled={
314
                  (!canValidateFiles && !canValidateMetadata) ||
275✔
315
                  // NOTE: No new data to validate if the submission is already submitted
316
                  dataSubmission?.status === "Submitted"
317
                }
318
              />
319
              <StyledRadioControl
320
                value="All"
321
                control={<StyledRadioButton readOnly={false} />}
322
                label="All Uploaded Data"
323
                disabled={!canValidateFiles && !canValidateMetadata}
190✔
324
              />
325
            </RadioGroup>
326
          </StyledRowContent>
327
        </StyledRow>
328
      </>
329
    </FlowWrapper>
330
  );
331
};
332

333
export default React.memo(ValidationControls);
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