From e310ec154175b8af721aadb0ea9e6e62f3235c09 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 14 Dec 2023 13:24:49 -0800 Subject: [PATCH 1/2] Add security review feature --- api/package-lock.json | 121 ++++++++++++---- .../administrative/submission/reviewed.ts | 2 +- .../administrative/submission/unreviewed.ts | 2 +- .../submission/{submissionId}/index.ts | 2 +- api/src/paths/submission/index.ts | 9 ++ .../index.ts | 75 ++++++++-- api/src/repositories/submission-repository.ts | 135 +++++++++--------- api/src/services/submission-service.ts | 24 ++-- app/src/contexts/submissionContext.tsx | 44 +++--- .../admin/dashboard/AdminDashboardRouter.tsx | 2 +- .../components/ReviewedSubmissionsTable.tsx | 4 +- .../components/UnreviewedSubmissionsTable.tsx | 6 +- .../submissions/AdminSubmissionPage.tsx | 53 +------ .../components/CompleteReviewDialog.tsx | 111 -------------- .../CompleteSecurityReviewDialog.tsx | 56 ++++++++ .../CompleteSecurityReviewStatusMessage.tsx | 55 +++++++ .../PublishSecurityReviewButton.tsx | 65 +++++++++ .../RemoveSecurityReviewDialog.tsx | 49 +++++++ .../components/SubmissionHeader.tsx | 112 ++++----------- .../SubmissionHeaderSecurityStatus.tsx | 81 +++++++++++ app/src/hooks/api/useDatasetApi.ts | 39 +---- app/src/hooks/api/useSubmissionsApi.ts | 64 +++++++-- app/src/hooks/useSubmissionContext.tsx | 19 +++ app/src/interfaces/useDatasetApi.interface.ts | 16 +++ .../interfaces/useSubmissionsApi.interface.ts | 10 +- database/package-lock.json | 60 ++++---- .../20231117000002_security_functions.ts | 17 +++ 27 files changed, 758 insertions(+), 475 deletions(-) rename api/src/paths/submission/{{submissionUUID} => {submissionId}}/index.ts (59%) delete mode 100644 app/src/features/submissions/components/CompleteReviewDialog.tsx create mode 100644 app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog.tsx create mode 100644 app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage.tsx create mode 100644 app/src/features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton.tsx create mode 100644 app/src/features/submissions/components/PublishSecurityReview/RemoveSecurityReviewDialog.tsx create mode 100644 app/src/features/submissions/components/SubmissionHeaderSecurityStatus.tsx create mode 100644 app/src/hooks/useSubmissionContext.tsx diff --git a/api/package-lock.json b/api/package-lock.json index 80f92bca8..7dcc29b7a 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -538,6 +538,36 @@ "strip-ansi": "^7.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -546,6 +576,21 @@ "ansi-regex": "^6.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } + } + }, "wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -555,6 +600,54 @@ "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } } } }, @@ -8719,16 +8812,6 @@ "strip-ansi": "^6.0.1" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, "string.prototype.padend": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz", @@ -8786,14 +8869,6 @@ "ansi-regex": "^5.0.1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", @@ -9831,16 +9906,6 @@ "strip-ansi": "^6.0.0" } }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/api/src/paths/administrative/submission/reviewed.ts b/api/src/paths/administrative/submission/reviewed.ts index 888c6e5e4..6900bd958 100644 --- a/api/src/paths/administrative/submission/reviewed.ts +++ b/api/src/paths/administrative/submission/reviewed.ts @@ -136,7 +136,7 @@ export function getReviewedSubmissionsForAdmins(): RequestHandler { return res.status(200).json(response); } catch (error) { - defaultLog.error({ label: 'getReviewedSubmissions', message: 'error', error }); + defaultLog.error({ label: 'getReviewedSubmissionsForAdmins', message: 'error', error }); await connection.rollback(); throw error; } finally { diff --git a/api/src/paths/administrative/submission/unreviewed.ts b/api/src/paths/administrative/submission/unreviewed.ts index ff2107be8..57a929a9a 100644 --- a/api/src/paths/administrative/submission/unreviewed.ts +++ b/api/src/paths/administrative/submission/unreviewed.ts @@ -139,7 +139,7 @@ export function getUnreviewedSubmissionsForAdmins(): RequestHandler { return res.status(200).json(response); } catch (error) { - defaultLog.error({ label: 'getUnreviewedSubmissions', message: 'error', error }); + defaultLog.error({ label: 'getUnreviewedSubmissionsForAdmins', message: 'error', error }); await connection.rollback(); throw error; } finally { diff --git a/api/src/paths/administrative/submission/{submissionId}/index.ts b/api/src/paths/administrative/submission/{submissionId}/index.ts index 34f13c882..d18fd53eb 100644 --- a/api/src/paths/administrative/submission/{submissionId}/index.ts +++ b/api/src/paths/administrative/submission/{submissionId}/index.ts @@ -148,7 +148,7 @@ export function patchSubmissionRecord(): RequestHandler { const submissionId = Number(req.params.submissionId); - const patch = req.body as PatchSubmissionRecord; + const patch = req.body.patch as PatchSubmissionRecord; try { await connection.open(); diff --git a/api/src/paths/submission/index.ts b/api/src/paths/submission/index.ts index 8bb7f18c6..70bdc3495 100644 --- a/api/src/paths/submission/index.ts +++ b/api/src/paths/submission/index.ts @@ -2,6 +2,7 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { getAPIUserDBConnection } from '../../database/db'; import { defaultErrorResponses } from '../../openapi/schemas/http-responses'; +import { SECURITY_APPLIED_STATUS } from '../../repositories/security-repository'; import { SubmissionService } from '../../services/submission-service'; import { getLogger } from '../../utils/logger'; @@ -81,6 +82,14 @@ GET.apiDoc = { revision_count: { type: 'integer', minimum: 0 + }, + security: { + type: 'string', + enum: [ + SECURITY_APPLIED_STATUS.SECURED, + SECURITY_APPLIED_STATUS.UNSECURED, + SECURITY_APPLIED_STATUS.PARTIALLY_SECURED + ] } } } diff --git a/api/src/paths/submission/{submissionUUID}/index.ts b/api/src/paths/submission/{submissionId}/index.ts similarity index 59% rename from api/src/paths/submission/{submissionUUID}/index.ts rename to api/src/paths/submission/{submissionId}/index.ts index 8243197cc..8ddb0d68f 100644 --- a/api/src/paths/submission/{submissionUUID}/index.ts +++ b/api/src/paths/submission/{submissionId}/index.ts @@ -5,12 +5,12 @@ import { defaultErrorResponses } from '../../../openapi/schemas/http-responses'; import { SubmissionService } from '../../../services/submission-service'; import { getLogger } from '../../../utils/logger'; -const defaultLog = getLogger('paths/submission/{submissionUUID}}'); +const defaultLog = getLogger('paths/submission/{submissionId}'); export const GET: Operation = [getSubmissionInformation()]; GET.apiDoc = { - description: 'retrieves submission data from the submission table', + description: 'Retrieves a submission record from the submission table', tags: ['eml'], security: [ { @@ -19,19 +19,19 @@ GET.apiDoc = { ], parameters: [ { - description: 'submission uuid', + description: 'Submission ID.', in: 'path', - name: 'submissionUUID', + name: 'submissionId', schema: { - type: 'string', - format: 'uuid' + type: 'integer', + minimum: 1 }, required: true } ], responses: { 200: { - description: 'Dataset metadata response object.', + description: 'A submission record and all child submission feature records.', content: { 'application/json': { schema: { @@ -40,10 +40,24 @@ GET.apiDoc = { properties: { submission: { type: 'object', - required: ['submission_id', 'uuid', 'security_review_timestamp', 'create_date'], + required: [ + 'submission_id', + 'uuid', + 'security_review_timestamp', + 'submitted_timestamp', + 'source_system', + 'name', + 'description', + 'create_date', + 'create_user', + 'update_date', + 'update_user', + 'revision_count' + ], properties: { submission_id: { - type: 'number' + type: 'integer', + minimum: 1 }, uuid: { type: 'string', @@ -51,12 +65,45 @@ GET.apiDoc = { }, security_review_timestamp: { type: 'string', - format: 'date-time', nullable: true }, + source_system: { + type: 'string' + }, + name: { + type: 'string', + maxLength: 200 + }, + description: { + type: 'string', + maxLength: 3000 + }, create_date: { + type: 'string' + }, + create_user: { + type: 'integer', + minimum: 1 + }, + update_date: { type: 'string', - format: 'date-time' + nullable: true + }, + update_user: { + type: 'integer', + minimum: 1, + nullable: true + }, + revision_count: { + type: 'integer', + minimum: 0 + }, + feature_type_id: { + type: 'integer', + minimum: 1 + }, + feature_type: { + type: 'string' } } }, @@ -99,7 +146,7 @@ GET.apiDoc = { }; /** - * Retrieves submission data from the submission table. + * Retrieves a submission record and all child submission feature records. * * @returns {RequestHandler} */ @@ -107,14 +154,14 @@ export function getSubmissionInformation(): RequestHandler { return async (req, res) => { const connection = req['keycloak_token'] ? getDBConnection(req['keycloak_token']) : getAPIUserDBConnection(); - const submissionUUID = String(req.params.submissionUUID); + const submissionId = Number(req.params.submissionId); try { await connection.open(); const submissionService = new SubmissionService(connection); - const result = await submissionService.getSubmissionAndFeaturesBySubmissionUUID(submissionUUID); + const result = await submissionService.getSubmissionAndFeaturesBySubmissionId(submissionId); await connection.commit(); diff --git a/api/src/repositories/submission-repository.ts b/api/src/repositories/submission-repository.ts index 2263c5293..95dcf8f1b 100644 --- a/api/src/repositories/submission-repository.ts +++ b/api/src/repositories/submission-repository.ts @@ -261,6 +261,13 @@ export const SubmissionWithSecurityRecord = SubmissionRecord.extend({ export type SubmissionWithSecurityRecord = z.infer; +export const SubmissionRecordWithRootFeatureType = SubmissionRecord.extend({ + feature_type_id: z.number(), + feature_type: z.string() +}); + +export type SubmissionRecordWithRootFeatureType = z.infer; + export const SubmissionMessageRecord = z.object({ submission_message_id: z.number(), submission_message_type_id: z.number(), @@ -1150,49 +1157,13 @@ export class SubmissionRepository extends BaseRepository { return response.rows[0]; } - /** - * Fetch a submission from uuid. - * - * @param {string} uuid - * @return {*} {Promise} - * @memberof SubmissionRepository - */ - async getSubmissionByUUID(uuid: string): Promise { - const sqlStatement = SQL` - SELECT - submission_id, - uuid, - security_review_timestamp, - create_date - FROM - submission - WHERE - uuid = ${uuid}; - `; - - const response = await this.connection.sql(sqlStatement); - - if (!response.rowCount) { - throw new ApiExecuteSQLError('Failed to get submission record', [ - 'SubmissionRepository->getSubmissionByUUID', - 'rowCount was null or undefined, expected rowCount != 0' - ]); - } - - return response.rows[0]; - } - /** * Get all submissions that are pending security review (are unreviewed). * - * @return {*} {(Promise< - * (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - * >)} + * @return {*} {Promise} * @memberof SubmissionRepository */ - async getUnreviewedSubmissionsForAdmins(): Promise< - (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - > { + async getUnreviewedSubmissionsForAdmins(): Promise { const sqlStatement = SQL` WITH w_unique_submissions as ( SELECT @@ -1223,10 +1194,7 @@ export class SubmissionRepository extends BaseRepository { ORDER BY submitted_timestamp DESC; `; - const response = await this.connection.sql( - sqlStatement, - SubmissionRecord.extend({ feature_type_id: z.number(), feature_type: z.string() }) - ); + const response = await this.connection.sql(sqlStatement, SubmissionRecordWithRootFeatureType); return response.rows; } @@ -1234,14 +1202,10 @@ export class SubmissionRepository extends BaseRepository { /** * Get all submissions that have completed security review (are reviewed). * - * @return {*} {(Promise< - * (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - * >)} + * @return {*} {Promise} * @memberof SubmissionRepository */ - async getReviewedSubmissionsForAdmins(): Promise< - (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - > { + async getReviewedSubmissionsForAdmins(): Promise { const sqlStatement = SQL` WITH w_unique_submissions as ( SELECT @@ -1272,16 +1236,13 @@ export class SubmissionRepository extends BaseRepository { ORDER BY submitted_timestamp DESC; `; - const response = await this.connection.sql( - sqlStatement, - SubmissionRecord.extend({ feature_type_id: z.number(), feature_type: z.string() }) - ); + const response = await this.connection.sql(sqlStatement, SubmissionRecordWithRootFeatureType); return response.rows; } /** - * Fetch a submission from uuid. + * Get all submission features by submission id. * * @param {number} submissionId * @return {*} {(Promise<(SubmissionFeatureRecord & { feature_type: string })[]>)} @@ -1291,17 +1252,18 @@ export class SubmissionRepository extends BaseRepository { submissionId: number ): Promise<(SubmissionFeatureRecord & { feature_type: string })[]> { const sqlStatement = SQL` - SELECT - sf.*, - (SELECT name FROM feature_type WHERE feature_type_id = sf.feature_type_id) AS feature_type, - sf.data, - sf.parent_submission_feature_id, - (SELECT sfs.submission_feature_security_id FROM submission_feature_security sfs WHERE sfs.submission_feature_id = sf.submission_feature_id) AS submission_feature_security_ids - FROM - submission_feature sf - WHERE - submission_id = ${submissionId}; - `; + SELECT + sf.*, + (SELECT name FROM feature_type WHERE feature_type_id = sf.feature_type_id) AS feature_type, + sf.data, + sf.parent_submission_feature_id, + (SELECT sfs.submission_feature_security_id FROM submission_feature_security sfs WHERE sfs.submission_feature_id = sf.submission_feature_id) AS submission_feature_security_ids + FROM + submission_feature sf + WHERE + submission_id = ${submissionId}; + `; + const response = await this.connection.sql( sqlStatement, SubmissionFeatureRecord.extend({ feature_type: z.string() }) @@ -1317,6 +1279,51 @@ export class SubmissionRepository extends BaseRepository { return response.rows; } + /** + * Get a submission record by id (with security status). + * + * @param {number} submissionId + * @return {*} {Promise} + * @memberof SubmissionRepository + */ + async getSubmissionRecordBySubmissionIdWithSecurity(submissionId: number): Promise { + const sqlStatement = SQL` + SELECT + submission.*, + CASE + WHEN submission.security_review_timestamp is null THEN ${SECURITY_APPLIED_STATUS.PENDING} + WHEN COUNT(submission_feature_security.submission_feature_security_id) = 0 THEN ${SECURITY_APPLIED_STATUS.UNSECURED} + WHEN COUNT(submission_feature_security.submission_feature_security_id) = COUNT(submission_feature.submission_feature_id) THEN ${SECURITY_APPLIED_STATUS.SECURED} + ELSE ${SECURITY_APPLIED_STATUS.PARTIALLY_SECURED} + END as security + FROM + submission + LEFT + JOIN submission_feature + ON + submission_feature.submission_id = submission.submission_id + LEFT JOIN + submission_feature_security + ON + submission_feature.submission_feature_id = submission_feature_security.submission_feature_id + WHERE + submission.submission_id = ${submissionId} + GROUP BY + submission.submission_id; + `; + + const response = await this.connection.sql(sqlStatement, SubmissionWithSecurityRecord); + + if (response.rowCount !== 1) { + throw new ApiExecuteSQLError('Failed to get submission record with security status', [ + 'SubmissionRepository->getSubmissionRecordBySubmissionIdWithSecurity', + `rowCount was ${response.rowCount}, expected rowCount === 1` + ]); + } + + return response.rows[0]; + } + /** * Get all submissions that have been reviewed (with security status) * diff --git a/api/src/services/submission-service.ts b/api/src/services/submission-service.ts index bb627f3a7..7cc753493 100644 --- a/api/src/services/submission-service.ts +++ b/api/src/services/submission-service.ts @@ -567,6 +567,17 @@ export class SubmissionService extends DBService { return this.submissionRepository.getReviewedSubmissionsForAdmins(); } + /** + * Get a submission record by id (with security status). + * + * @param {number} submissionId + * @return {*} {Promise} + * @memberof SubmissionService + */ + async getSubmissionRecordBySubmissionIdWithSecurity(submissionId: number): Promise { + return this.submissionRepository.getSubmissionRecordBySubmissionIdWithSecurity(submissionId); + } + /** * Get all submissions (with security status) that have been reviewed. * @@ -576,21 +587,18 @@ export class SubmissionService extends DBService { async getReviewedSubmissionsWithSecurity(): Promise { return this.submissionRepository.getReviewedSubmissionsWithSecurity(); } + /* * Retrieves submission data from the submission table. * - * @param {string} submissionUUID + * @param {number} submissionId * @return {*} {Promise} TODO: type * @memberof DatasetService */ - async getSubmissionAndFeaturesBySubmissionUUID(submissionUUID: string): Promise { - const submission = await this.submissionRepository.getSubmissionByUUID(submissionUUID); - - if (!submission.submission_id) { - throw new Error(`No submission found for submission ${submissionUUID}`); - } + async getSubmissionAndFeaturesBySubmissionId(submissionId: number): Promise { + const submission = await this.submissionRepository.getSubmissionRecordBySubmissionIdWithSecurity(submissionId); - const features = await this.submissionRepository.getSubmissionFeaturesBySubmissionId(submission.submission_id); + const features = await this.submissionRepository.getSubmissionFeaturesBySubmissionId(submissionId); const dataset = []; const sampleSites = []; diff --git a/app/src/contexts/submissionContext.tsx b/app/src/contexts/submissionContext.tsx index b9733e7c0..e06b96c7c 100644 --- a/app/src/contexts/submissionContext.tsx +++ b/app/src/contexts/submissionContext.tsx @@ -8,57 +8,55 @@ export interface ISubmissionContext { /** * The Data Loader used to load submission data * - * @type {DataLoader<[submissionUUID: string], IGetSubmissionResponse, unknown>} + * @type {DataLoader<[submissionId: number], IGetSubmissionResponse, unknown>} * @memberof ISubmissionContext */ - submissionDataLoader: DataLoader<[submissionUUID: string], IGetSubmissionResponse, unknown>; + submissionDataLoader: DataLoader<[submissionId: number], IGetSubmissionResponse, unknown>; /** - * The submission UUID + * The submission id. * - * @type {string} + * @type {number} * @memberof ISubmissionContext */ - submissionUUID: string; + submissionId: number; } -export const SubmissionContext = React.createContext({ - submissionDataLoader: {} as DataLoader<[submissionUUID: string], IGetSubmissionResponse, unknown>, - submissionUUID: '' -}); +export const SubmissionContext = React.createContext(undefined); export const SubmissionContextProvider: React.FC = (props) => { const biohubApi = useApi(); + const submissionDataLoader = useDataLoader(biohubApi.submissions.getSubmission); - const urlParams: Record = useParams(); + const urlParams = useParams(); + + const submissionId = Number(urlParams['submission_id']); - if (!urlParams['submission_uuid']) { + if (!submissionId) { throw new Error( - "The submission UUID found in SubmissionContextProvider was invalid. Does your current React route provide an 'id' parameter?" + "The submission ID found in SubmissionContextProvider was invalid. Does your current React route provide a 'submission_id' parameter?" ); } - const submissionUUID = urlParams['submission_uuid'] as string; - - submissionDataLoader.load(submissionUUID); + submissionDataLoader.load(submissionId); /** - * Refreshes the current submission object whenever the current submission UUID changes from the currently loaded submission. + * Refreshes the current submission object whenever the current submission id changes from the currently loaded submission. */ useEffect(() => { - if (submissionUUID && submissionUUID !== submissionDataLoader.data?.submission.uuid) { - submissionDataLoader.refresh(submissionUUID); + if (submissionId && submissionId !== submissionDataLoader.data?.submission.submission_id) { + submissionDataLoader.refresh(submissionId); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [submissionUUID]); + }, [submissionId]); - const surveyContext: ISubmissionContext = useMemo(() => { + const submissionContext: ISubmissionContext = useMemo(() => { return { submissionDataLoader, - submissionUUID + submissionId }; - }, [submissionDataLoader, submissionUUID]); + }, [submissionDataLoader, submissionId]); - return {props.children}; + return {props.children}; }; diff --git a/app/src/features/admin/dashboard/AdminDashboardRouter.tsx b/app/src/features/admin/dashboard/AdminDashboardRouter.tsx index 1c187e5cf..c73637da7 100644 --- a/app/src/features/admin/dashboard/AdminDashboardRouter.tsx +++ b/app/src/features/admin/dashboard/AdminDashboardRouter.tsx @@ -17,7 +17,7 @@ const AdminDashboardRouter: React.FC = () => { - + diff --git a/app/src/features/admin/dashboard/components/ReviewedSubmissionsTable.tsx b/app/src/features/admin/dashboard/components/ReviewedSubmissionsTable.tsx index 5fee7e874..f8f00ca16 100644 --- a/app/src/features/admin/dashboard/components/ReviewedSubmissionsTable.tsx +++ b/app/src/features/admin/dashboard/components/ReviewedSubmissionsTable.tsx @@ -15,7 +15,7 @@ import { getFormattedDate, pluralize as p } from 'utils/Utils'; const ReviewedSubmissionsTable = () => { const biohubApi = useApi(); - const reviewedSubmissionsDataLoader = useDataLoader(() => biohubApi.dataset.getReviewedSubmissions()); + const reviewedSubmissionsDataLoader = useDataLoader(() => biohubApi.submissions.getReviewedSubmissions()); reviewedSubmissionsDataLoader.load(); @@ -71,7 +71,7 @@ const ReviewedSubmissionsTable = () => { {submissionRecords.map((submissionRecord) => { return ( - + { const biohubApi = useApi(); - const unreviewedSubmissionsDataLoader = useDataLoader(() => biohubApi.dataset.getUnreviewedSubmissions()); + const unreviewedSubmissionsDataLoader = useDataLoader(() => biohubApi.submissions.getUnreviewedSubmissions()); unreviewedSubmissionsDataLoader.load(); @@ -72,7 +72,7 @@ const UnreviewedSubmissionsTable = () => { {submissionRecords.map((submissionRecord) => { return ( - + { component={RouterLink} variant="contained" color="primary" - to={`/admin/dashboard/submissions/${submissionRecord.uuid}`} + to={`/admin/dashboard/submissions/${submissionRecord.submission_id}`} sx={{ flex: '0 0 auto', minWidth: '7rem' diff --git a/app/src/features/submissions/AdminSubmissionPage.tsx b/app/src/features/submissions/AdminSubmissionPage.tsx index b52591e59..84340ac81 100644 --- a/app/src/features/submissions/AdminSubmissionPage.tsx +++ b/app/src/features/submissions/AdminSubmissionPage.tsx @@ -1,14 +1,11 @@ import Box from '@mui/material/Box'; import Container from '@mui/material/Container'; -import { SubmissionContext } from 'contexts/submissionContext'; -import { useContext, useState } from 'react'; -import CompleteReviewDialog from './components/CompleteReviewDialog'; -import ManageSecurityReasonsDialog from './components/ManageSecurityReasonsDialog'; +import SubmissionHeader from 'features/submissions/components/SubmissionHeader'; +import { useSubmissionContext } from 'hooks/useSubmissionContext'; import SubmissionDataGrid from './components/SubmissionDataGrid'; -import SubmissionHeader from './components/SubmissionHeader'; const AdminSubmissionPage = () => { - const submissionContext = useContext(SubmissionContext); + const submissionContext = useSubmissionContext(); const submissionDataLoader = submissionContext.submissionDataLoader; const features = submissionDataLoader.data?.features; @@ -18,51 +15,9 @@ const AdminSubmissionPage = () => { const animals = features?.animals; const observations = features?.observations; - const [openSecurityReasonsDialog, setOpenSecurityReasonsDialog] = useState(false); - const [openCompleteReviewDialog, setOpenCompleteReviewDialog] = useState(false); - - const submitSecurity = async (values: any) => { - console.log('values', values); - }; - return ( - setOpenCompleteReviewDialog(false)} - onSubmit={async (values) => { - setOpenCompleteReviewDialog(false); - submitSecurity(values); - }} - submissionSuccessDialogTitle="Submission Completed" - submissionSuccessDialogText="Submission has been completed successfully." - noSubmissionDataDialogTitle="No Submission Data" - noSubmissionDataDialogText="No submission data found." - /> - setOpenSecurityReasonsDialog(false)} - onSubmit={async (values) => { - setOpenSecurityReasonsDialog(false); - submitSecurity(values); - }} - submissionSuccessDialogTitle="Submission Security Review Submitted" - submissionSuccessDialogText="Submission security review has been submitted successfully." - noSubmissionDataDialogTitle="No Submission Data" - noSubmissionDataDialogText="No submission data found." - /> - {/* TODO: create unsecure dialog */} - { - setOpenSecurityReasonsDialog(open); - }} - openCompleteReviewDialog={(open: boolean) => { - setOpenCompleteReviewDialog(open); - }} - openUnsecureRecordsDialog={(open: boolean) => { - setOpenSecurityReasonsDialog(open); - }} - /> + diff --git a/app/src/features/submissions/components/CompleteReviewDialog.tsx b/app/src/features/submissions/components/CompleteReviewDialog.tsx deleted file mode 100644 index 4bebd916d..000000000 --- a/app/src/features/submissions/components/CompleteReviewDialog.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { mdiExclamation } from '@mdi/js'; -import Icon from '@mdi/react'; -import { LoadingButton } from '@mui/lab'; -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; -import useTheme from '@mui/material/styles/useTheme'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { Box } from '@mui/system'; -import ComponentDialog from 'components/dialog/ComponentDialog'; -import { PropsWithChildren, useState } from 'react'; - -/** - * - * - * @export - * @interface IManageSecurityReasonsProps - */ -export interface IManageSecurityReasonsProps { - /** - * Set to `true` to open the dialog, `false` to close the dialog. - * - * @type {boolean} - * @memberof IManageSecurityReasonsProps - */ - open: boolean; - /** - * Callback fired if the dialog is closed. - * - * @memberof IManageSecurityReasonsProps - */ - onClose: () => void; - /** - * Callback fired when submission is made to Biohub - * - * @memberof IManageSecurityReasonsProps - */ - onSubmit: (values: any) => Promise; - - submissionSuccessDialogTitle: string; - submissionSuccessDialogText: string; - noSubmissionDataDialogTitle: string; - noSubmissionDataDialogText: string; -} - -/** - * TODO: FINISH COMPLETE REVIEW DIALOG - * - * @param {*} props - * @return {*} - */ -const CompleteReviewDialog = (props: PropsWithChildren) => { - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const [showSuccessDialog, setShowSuccessDialog] = useState(false); - const [showNoInformationDialog, setShowNoInformationDialog] = useState(false); - - return ( - <> - setShowSuccessDialog(false)}> - {props.submissionSuccessDialogText} - - - setShowNoInformationDialog(false)}> - {props.noSubmissionDataDialogText} - - - - <> - Complete Review - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam at porttitor sem. Aliquam erat volutpat. - Donec placerat nisl magna, et faucibus arcu condimentum sed. - - - {/**TODO: fix color */} - Open access to all Records Users will be able to access and download all information for this dataset. - - - - - Complete - - - - - - - ); -}; - -export default CompleteReviewDialog; diff --git a/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog.tsx b/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog.tsx new file mode 100644 index 000000000..4a303ac49 --- /dev/null +++ b/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog.tsx @@ -0,0 +1,56 @@ +import { LoadingButton } from '@mui/lab'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import useTheme from '@mui/material/styles/useTheme'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import CompleteSecurityReviewStatusMessage from 'features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage'; +import { SubmissionRecordWithSecurity } from 'interfaces/useDatasetApi.interface'; + +export interface ICompleteSecurityReviewDialogProps { + submission: SubmissionRecordWithSecurity; + open: boolean; + onComplete: () => void; + onCancel: () => void; +} + +const CompleteSecurityReviewDialog = (props: ICompleteSecurityReviewDialogProps) => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + + const { submission, open, onComplete, onCancel } = props; + + return ( + + Complete Review + + + + Completing the security review will make all records of this submission available to the users of BioHub. + Records with no security rules will be accessible by all users. Records with one or more security rules will + be restricted pending approval by a BioHub Administrator. + + + + + + onComplete()} color="primary" variant="contained"> + Complete + + + + + ); +}; + +export default CompleteSecurityReviewDialog; diff --git a/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage.tsx b/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage.tsx new file mode 100644 index 000000000..5ea75c24a --- /dev/null +++ b/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage.tsx @@ -0,0 +1,55 @@ +import { mdiAlertCircleOutline, mdiLock, mdiLockAlertOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import { Box } from '@mui/system'; +import { SECURITY_APPLIED_STATUS, SubmissionRecordWithSecurity } from 'interfaces/useDatasetApi.interface'; + +export interface ICompleteSecurityReviewStatusMessageProps { + submission: SubmissionRecordWithSecurity; +} + +const CompleteSecurityReviewStatusMessage = (props: ICompleteSecurityReviewStatusMessageProps) => { + const { submission } = props; + + if (submission.security === SECURITY_APPLIED_STATUS.SECURED) { + return ( + + }> + + {'All records secured'} + + {'Access to secured records will be restricted pending approval by a BioHub Administrator.'} + + + ); + } + + if (submission.security === SECURITY_APPLIED_STATUS.PARTIALLY_SECURED) { + return ( + + }> + + {'Some records are secured'} + + { + 'Users can access unsecured records. Access to secured records will be restricted pending approval by a BioHub Administrator.' + } + + + ); + } + + return ( + + }> + + {'Open access to all records'} + + {'Users can access all records.'} + + + ); +}; + +export default CompleteSecurityReviewStatusMessage; diff --git a/app/src/features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton.tsx b/app/src/features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton.tsx new file mode 100644 index 000000000..524a3b5a9 --- /dev/null +++ b/app/src/features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton.tsx @@ -0,0 +1,65 @@ +import Button from '@mui/material/Button'; +import CompleteSecurityReviewDialog from 'features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog'; +import RemoveSecurityReviewDialog from 'features/submissions/components/PublishSecurityReview/RemoveSecurityReviewDialog'; +import { SubmissionRecordWithSecurity } from 'interfaces/useDatasetApi.interface'; +import { useState } from 'react'; + +export interface IPublishSecurityReviewButtonProps { + submission: SubmissionRecordWithSecurity; + onComplete: () => Promise; + onRemove: () => Promise; +} + +const PublishSecurityReviewButton = (props: IPublishSecurityReviewButtonProps) => { + const [isCompleteReviewDialogOpen, setIsCompleteReviewDialogOpen] = useState(false); + const [isRemoveReviewDialogOpen, setIsRemoveReviewDialogOpen] = useState(false); + + const { submission, onComplete, onRemove } = props; + + return ( + <> + {(submission.security_review_timestamp && ( + <> + { + onRemove(); + setIsRemoveReviewDialogOpen(false); + }} + onCancel={() => setIsRemoveReviewDialogOpen(false)} + /> + + + )) || ( + <> + { + onComplete(); + setIsCompleteReviewDialogOpen(false); + }} + onCancel={() => setIsCompleteReviewDialogOpen(false)} + /> + + + )} + + ); +}; + +export default PublishSecurityReviewButton; diff --git a/app/src/features/submissions/components/PublishSecurityReview/RemoveSecurityReviewDialog.tsx b/app/src/features/submissions/components/PublishSecurityReview/RemoveSecurityReviewDialog.tsx new file mode 100644 index 000000000..88c65063d --- /dev/null +++ b/app/src/features/submissions/components/PublishSecurityReview/RemoveSecurityReviewDialog.tsx @@ -0,0 +1,49 @@ +import { LoadingButton } from '@mui/lab'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import useTheme from '@mui/material/styles/useTheme'; +import useMediaQuery from '@mui/material/useMediaQuery'; + +export interface IRemoveSecurityReviewDialogProps { + open: boolean; + onRemove: () => void; + onCancel: () => void; +} + +const RemoveSecurityReviewDialog = (props: IRemoveSecurityReviewDialogProps) => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + + const { open, onRemove, onCancel } = props; + + return ( + + Remove Review + + + + Removing the security review will make all records of this submission unavailable to users of Biohub. + + + + onRemove()} color="primary" variant="contained"> + Remove Review + + + + + ); +}; + +export default RemoveSecurityReviewDialog; diff --git a/app/src/features/submissions/components/SubmissionHeader.tsx b/app/src/features/submissions/components/SubmissionHeader.tsx index 9d3806778..842613b94 100644 --- a/app/src/features/submissions/components/SubmissionHeader.tsx +++ b/app/src/features/submissions/components/SubmissionHeader.tsx @@ -1,24 +1,17 @@ -import { mdiChevronDown, mdiCog, mdiLock, mdiLockOpenVariantOutline } from '@mdi/js'; -import Icon from '@mdi/react'; import Breadcrumbs from '@mui/material/Breadcrumbs'; -import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import Link from '@mui/material/Link'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import BaseHeader from 'components/layout/header/BaseHeader'; -import { SubmissionContext } from 'contexts/submissionContext'; -import moment from 'moment'; -import React, { useContext, useState } from 'react'; +import PublishSecurityReviewButton from 'features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton'; +import SubmissionHeaderSecurityStatus from 'features/submissions/components/SubmissionHeaderSecurityStatus'; +import { useApi } from 'hooks/useApi'; +import { useSubmissionContext } from 'hooks/useSubmissionContext'; import { Link as RouterLink } from 'react-router-dom'; export interface ISubmissionHeaderProps { - openSecureRecordsDialog: (open: boolean) => void; - openCompleteReviewDialog: (open: boolean) => void; - openUnsecureRecordsDialog: (open: boolean) => void; + selectedFeatures: number[]; } /** @@ -27,11 +20,11 @@ export interface ISubmissionHeaderProps { * @return {*} */ const SubmissionHeader = (props: ISubmissionHeaderProps) => { - const submissionContext = useContext(SubmissionContext); + const submissionContext = useSubmissionContext(); - const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const api = useApi(); - const submissionUUID = submissionContext.submissionUUID; + const submissionUUID = submissionContext.submissionDataLoader.data?.submission.uuid; const submissionDataLoader = submissionContext.submissionDataLoader; if (!submissionDataLoader.data) { @@ -42,6 +35,16 @@ const SubmissionHeader = (props: ISubmissionHeaderProps) => { const features = submissionDataLoader.data?.features; const dataset = features.dataset[0]; + const onSecurityReviewComplete = async () => { + await api.submissions.updateSubmissionRecord(submissionContext.submissionId, { security_reviewed: true }); + submissionContext.submissionDataLoader.refresh(submissionContext.submissionId); + }; + + const onSecurityReviewRemove = async () => { + await api.submissions.updateSubmissionRecord(submissionContext.submissionId, { security_reviewed: false }); + submissionContext.submissionDataLoader.refresh(submissionContext.submissionId); + }; + return ( <> { breadCrumb={ - DASHBOARD + Dashboard {dataset?.data.name} | {submissionUUID} @@ -58,78 +61,19 @@ const SubmissionHeader = (props: ISubmissionHeaderProps) => { } subTitle={ - - {submission?.security_review_timestamp ? ( - <> - - - SECURED: {submission?.security_review_timestamp} - - - ) : ( - <> - - - PENDING REVIEW | SUBMITTED:{' '} - {submission?.create_date ? moment(submission?.create_date).format('YYYY-MM-DD') : 'N/A'} - - - )} - + } buttonJSX={ - <> - - + + {/* */} - - - - setMenuAnchorEl(null)}> - props.openSecureRecordsDialog(true)}> - - - - Secure Records - - props.openUnsecureRecordsDialog(true)}> - - - - Unsecure Records - - - + + } /> diff --git a/app/src/features/submissions/components/SubmissionHeaderSecurityStatus.tsx b/app/src/features/submissions/components/SubmissionHeaderSecurityStatus.tsx new file mode 100644 index 000000000..72f59aae0 --- /dev/null +++ b/app/src/features/submissions/components/SubmissionHeaderSecurityStatus.tsx @@ -0,0 +1,81 @@ +import { mdiClipboardTextOutline, mdiLock, mdiLockAlertOutline, mdiLockOpenVariantOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import Typography from '@mui/material/Typography'; +import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import { SECURITY_APPLIED_STATUS, SubmissionRecordWithSecurity } from 'interfaces/useDatasetApi.interface'; +import { getFormattedDate } from 'utils/Utils'; + +export interface ISubmissionHeaderSecurityStatusProps { + submission: SubmissionRecordWithSecurity; +} + +const SubmissionHeaderSecurityStatus = (props: ISubmissionHeaderSecurityStatusProps) => { + const { submission } = props; + + if (submission.security === SECURITY_APPLIED_STATUS.UNSECURED) { + return ( + <> + + + + + Unsecured + + + Published:  + {getFormattedDate(DATE_FORMAT.ShortDateFormat, submission.security_review_timestamp as string)} + + + ); + } + + if (submission.security === SECURITY_APPLIED_STATUS.SECURED) { + return ( + <> + + + + + Secured + + + Published:  + {getFormattedDate(DATE_FORMAT.ShortDateFormat, submission.security_review_timestamp as string)} + + + ); + } + + if (submission.security === SECURITY_APPLIED_STATUS.PARTIALLY_SECURED) { + return ( + <> + + + + + Partially Secured + + + Published:  + {getFormattedDate(DATE_FORMAT.ShortDateFormat, submission.security_review_timestamp as string)} + + + ); + } + + return ( + <> + + + + + Pending Review + + + Submitted: {getFormattedDate(DATE_FORMAT.ShortDateFormat, submission.create_date as string)} + + + ); +}; + +export default SubmissionHeaderSecurityStatus; diff --git a/app/src/hooks/api/useDatasetApi.ts b/app/src/hooks/api/useDatasetApi.ts index d4a322df4..a9c964f2c 100644 --- a/app/src/hooks/api/useDatasetApi.ts +++ b/app/src/hooks/api/useDatasetApi.ts @@ -1,10 +1,5 @@ import { AxiosInstance } from 'axios'; -import { - IArtifact, - IHandlebarsTemplates, - IListRelatedDatasetsResponse, - SubmissionRecord -} from 'interfaces/useDatasetApi.interface'; +import { IArtifact, IHandlebarsTemplates, IListRelatedDatasetsResponse } from 'interfaces/useDatasetApi.interface'; import { IKeywordSearchResponse } from 'interfaces/useSearchApi.interface'; /** @@ -25,36 +20,6 @@ const useDatasetApi = (axios: AxiosInstance) => { return data; }; - /** - * Fetch all submissions that have not completed security review. - * - * @return {*} {(Promise< - * (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - * >)} - */ - const getUnreviewedSubmissions = async (): Promise< - (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - > => { - const { data } = await axios.get(`api/administrative/submission/unreviewed`); - - return data; - }; - - /** - * Fetch all submissions that have completed security review. - * - * @return {*} {(Promise< - * (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - * >)} - */ - const getReviewedSubmissions = async (): Promise< - (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - > => { - const { data } = await axios.get(`api/administrative/submission/reviewed`); - - return data; - }; - /** * Fetch dataset metadata by datasetId. * @@ -116,8 +81,6 @@ const useDatasetApi = (axios: AxiosInstance) => { return { listAllDatasets, - getUnreviewedSubmissions, - getReviewedSubmissions, getDatasetEML, getDatasetArtifacts, getArtifactSignedUrl, diff --git a/app/src/hooks/api/useSubmissionsApi.ts b/app/src/hooks/api/useSubmissionsApi.ts index 2ba53007e..228040f6c 100644 --- a/app/src/hooks/api/useSubmissionsApi.ts +++ b/app/src/hooks/api/useSubmissionsApi.ts @@ -1,6 +1,6 @@ import { AxiosInstance } from 'axios'; -import { SECURITY_APPLIED_STATUS } from 'interfaces/useDatasetApi.interface'; -import { IListSubmissionsResponse, ISubmission } from 'interfaces/useSubmissionsApi.interface'; +import { SECURITY_APPLIED_STATUS, SubmissionRecord } from 'interfaces/useDatasetApi.interface'; +import { IGetSubmissionResponse, IListSubmissionsResponse, ISubmission } from 'interfaces/useSubmissionsApi.interface'; /** * Returns a set of supported CRUD api methods submissions. @@ -72,14 +72,57 @@ const useSubmissionsApi = (axios: AxiosInstance) => { }; /** - * Fetch submission data by submissionUUID. + * Fetch submission data by submission id. * - * @param {string} submissionUUID - * @return {*} {Promise} //TODO: type + * @param {number} submissionId + * @return {*} {Promise} */ - const getSubmission = async (submissionUUID: string): Promise => { - const { data } = await axios.get(`api/submission/${submissionUUID}`); - console.log('data', data); + const getSubmission = async (submissionId: number): Promise => { + const { data } = await axios.get(`api/submission/${submissionId}`); + + return data; + }; + + /** + * Fetch all submissions that have not completed security review. + * + * @return {*} {(Promise< + * (SubmissionRecord & { feature_type_id: number; feature_type: string })[] + * >)} + */ + const getUnreviewedSubmissions = async (): Promise< + (SubmissionRecord & { feature_type_id: number; feature_type: string })[] + > => { + const { data } = await axios.get(`api/administrative/submission/unreviewed`); + + return data; + }; + + /** + * Fetch all submissions that have completed security review. + * + * @return {*} {(Promise< + * (SubmissionRecord & { feature_type_id: number; feature_type: string })[] + * >)} + */ + const getReviewedSubmissions = async (): Promise< + (SubmissionRecord & { feature_type_id: number; feature_type: string })[] + > => { + const { data } = await axios.get(`api/administrative/submission/reviewed`); + + return data; + }; + + /** + * Update (patch) a submission record. + * + * @param {number} submissionId + * @param {{ security_reviewed: boolean }} patch + * @return {*} + */ + const updateSubmissionRecord = async (submissionId: number, patch: { security_reviewed: boolean }) => { + const { data } = await axios.patch(`api/administrative/submission/${submissionId}`, { patch }); + return data; }; @@ -88,7 +131,10 @@ const useSubmissionsApi = (axios: AxiosInstance) => { getSignedUrl, listReviewedSubmissions, getSubmissionDownloadPackage, - getSubmission + getSubmission, + getUnreviewedSubmissions, + getReviewedSubmissions, + updateSubmissionRecord }; }; diff --git a/app/src/hooks/useSubmissionContext.tsx b/app/src/hooks/useSubmissionContext.tsx new file mode 100644 index 000000000..98b403a3c --- /dev/null +++ b/app/src/hooks/useSubmissionContext.tsx @@ -0,0 +1,19 @@ +import { useContext } from 'react'; +import { ISubmissionContext, SubmissionContext } from '../contexts/submissionContext'; + +/** + * Returns an instance of `ISubmissionContext` from `SubmissionContext`. + * + * @return {*} {ISubmissionContext} + */ +export const useSubmissionContext = (): ISubmissionContext => { + const context = useContext(SubmissionContext); + + if (!context) { + throw Error( + 'SubmissionContext is undefined, please verify you are calling useSubmissionContext() as child of an component.' + ); + } + + return context; +}; diff --git a/app/src/interfaces/useDatasetApi.interface.ts b/app/src/interfaces/useDatasetApi.interface.ts index facbc9e75..13600e6c9 100644 --- a/app/src/interfaces/useDatasetApi.interface.ts +++ b/app/src/interfaces/useDatasetApi.interface.ts @@ -72,6 +72,22 @@ export type SubmissionRecord = { update_user: number | null; revision_count: number; }; + +export type SubmissionRecordWithSecurity = { + submission_id: number; + uuid: string; + security_review_timestamp: string | null; + source_system: string; + name: string; + description: string; + create_date: string; + create_user: number; + update_date: string | null; + update_user: number | null; + revision_count: number; + security: SECURITY_APPLIED_STATUS; +}; + export interface ISubmission { submission_id: number; uuid: string; diff --git a/app/src/interfaces/useSubmissionsApi.interface.ts b/app/src/interfaces/useSubmissionsApi.interface.ts index 321dfb330..3b122877f 100644 --- a/app/src/interfaces/useSubmissionsApi.interface.ts +++ b/app/src/interfaces/useSubmissionsApi.interface.ts @@ -1,4 +1,4 @@ -import { SECURITY_APPLIED_STATUS } from './useDatasetApi.interface'; +import { SECURITY_APPLIED_STATUS, SubmissionRecordWithSecurity } from './useDatasetApi.interface'; export type IListSubmissionsResponse = Array<{ submission_id: number; @@ -29,12 +29,6 @@ export interface ISubmission { security: SECURITY_APPLIED_STATUS; } -export interface ISubmissionFeature { - submission_id: number; - uuid: string; - security_review_timestamp: string; - create_date: string; -} export interface IFeature { submission_feature_id: number; submission_id: number; @@ -43,7 +37,7 @@ export interface IFeature { parent_submission_feature_id: number | null; } export interface IGetSubmissionResponse { - submission: ISubmissionFeature; + submission: SubmissionRecordWithSecurity; features: { dataset: IFeature[]; sampleSites: IFeature[]; diff --git a/database/package-lock.json b/database/package-lock.json index aa9d5dea2..2148757cf 100644 --- a/database/package-lock.json +++ b/database/package-lock.json @@ -503,7 +503,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, "colorette": { @@ -519,7 +519,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "core-util-is": { @@ -927,7 +927,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, "fastq": { @@ -995,7 +995,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, "function-bind": { @@ -1018,7 +1018,7 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, "functions-have-names": { @@ -1172,7 +1172,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, "has-property-descriptors": { @@ -1238,13 +1238,13 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { "once": "^1.3.0", @@ -1287,7 +1287,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, "is-bigint": { @@ -1335,7 +1335,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, "is-fullwidth-code-point": { @@ -1442,7 +1442,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, "js-tokens": { @@ -1476,7 +1476,7 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, "jsonparse": { @@ -1519,7 +1519,7 @@ "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -1542,7 +1542,7 @@ "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", "dev": true }, "lru-cache": { @@ -1563,7 +1563,7 @@ "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", "dev": true }, "merge2": { @@ -1599,7 +1599,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, "nice-try": { @@ -1670,7 +1670,7 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, "semver": { @@ -1682,7 +1682,7 @@ "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "requires": { "shebang-regex": "^1.0.0" @@ -1691,7 +1691,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, "which": { @@ -1732,7 +1732,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { "wrappy": "1" @@ -1769,7 +1769,7 @@ "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "dev": true, "requires": { "error-ex": "^1.3.1", @@ -1779,7 +1779,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, "path-key": { @@ -1869,7 +1869,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true }, "postgres-array": { @@ -1880,7 +1880,7 @@ "postgres-bytea": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" }, "postgres-date": { "version": "1.0.7", @@ -1949,7 +1949,7 @@ "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", "dev": true, "requires": { "load-json-file": "^4.0.0", @@ -2073,7 +2073,7 @@ "semver": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", - "integrity": "sha512-VyFUffiBx8hABJ9HYSTXLRwyZtdDHMzMtFmID1aiNAD2BZppBmJm0Hqw3p2jkgxP9BNt1pQ9RnC49P0EcXf6cA==" + "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" }, "shebang-command": { "version": "2.0.0", @@ -2190,7 +2190,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, "string-width": { @@ -2269,7 +2269,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, "strip-json-comments": { @@ -2333,7 +2333,7 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, "through": { @@ -2518,7 +2518,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, "xtend": { diff --git a/database/src/migrations/20231117000002_security_functions.ts b/database/src/migrations/20231117000002_security_functions.ts index ae00ec191..9073c62cc 100644 --- a/database/src/migrations/20231117000002_security_functions.ts +++ b/database/src/migrations/20231117000002_security_functions.ts @@ -51,6 +51,23 @@ export async function up(knex: Knex): Promise { END; $$; + -- Executes a single security_datetime condition against the provided submission_feature_id + -- Note: Returns true if the condition hit (applied). + CREATE OR REPLACE FUNCTION evaluate_security_datetime_condition(rule_name VARCHAR, submission_feature_id integer) + RETURNS TABLE (result BOOLEAN) + language plpgsql + set client_min_messages = warning + AS $$ + DECLARE + comparator TEXT; + value timestamptz(6); + BEGIN + SELECT security_datetime.comparator, security_datetime.value INTO comparator, value FROM security_datetime WHERE name = evaluate_security_datetime_condition.rule_name; + + RETURN QUERY execute format('SELECT CASE WHEN EXISTS (SELECT 1 FROM search_datetime WHERE submission_feature_id = %s and value %s ''%s'') THEN TRUE ELSE FALSE END', evaluate_security_datetime_condition.submission_feature_id, comparator, value); + END; + $$; + -- Executes a single security_spatial condition against the provided submission_feature_id -- Note: Returns true if the condition hit (applied). -- Note: SRID of both geometries must be the same (prefer 4326) From 990d05b3515a1864bed2ea1d087d2589549c3a89 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 14 Dec 2023 13:31:11 -0800 Subject: [PATCH 2/2] Update interfaces --- .../CompleteSecurityReviewDialog.tsx | 2 +- .../CompleteSecurityReviewStatusMessage.tsx | 3 +- .../PublishSecurityReviewButton.tsx | 2 +- .../SubmissionHeaderSecurityStatus.tsx | 3 +- app/src/hooks/api/useSubmissionsApi.ts | 25 +++++++--------- app/src/interfaces/useDatasetApi.interface.ts | 29 ------------------- .../interfaces/useSubmissionsApi.interface.ts | 25 +++++++++++++++- 7 files changed, 41 insertions(+), 48 deletions(-) diff --git a/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog.tsx b/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog.tsx index 4a303ac49..1e240ea3d 100644 --- a/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog.tsx +++ b/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog.tsx @@ -8,7 +8,7 @@ import DialogTitle from '@mui/material/DialogTitle'; import useTheme from '@mui/material/styles/useTheme'; import useMediaQuery from '@mui/material/useMediaQuery'; import CompleteSecurityReviewStatusMessage from 'features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage'; -import { SubmissionRecordWithSecurity } from 'interfaces/useDatasetApi.interface'; +import { SubmissionRecordWithSecurity } from 'interfaces/useSubmissionsApi.interface'; export interface ICompleteSecurityReviewDialogProps { submission: SubmissionRecordWithSecurity; diff --git a/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage.tsx b/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage.tsx index 5ea75c24a..106a1976a 100644 --- a/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage.tsx +++ b/app/src/features/submissions/components/PublishSecurityReview/CompleteSecurityReviewStatusMessage.tsx @@ -3,7 +3,8 @@ import Icon from '@mdi/react'; import Alert from '@mui/material/Alert'; import AlertTitle from '@mui/material/AlertTitle'; import { Box } from '@mui/system'; -import { SECURITY_APPLIED_STATUS, SubmissionRecordWithSecurity } from 'interfaces/useDatasetApi.interface'; +import { SECURITY_APPLIED_STATUS } from 'interfaces/useDatasetApi.interface'; +import { SubmissionRecordWithSecurity } from 'interfaces/useSubmissionsApi.interface'; export interface ICompleteSecurityReviewStatusMessageProps { submission: SubmissionRecordWithSecurity; diff --git a/app/src/features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton.tsx b/app/src/features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton.tsx index 524a3b5a9..d6fe769d9 100644 --- a/app/src/features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton.tsx +++ b/app/src/features/submissions/components/PublishSecurityReview/PublishSecurityReviewButton.tsx @@ -1,7 +1,7 @@ import Button from '@mui/material/Button'; import CompleteSecurityReviewDialog from 'features/submissions/components/PublishSecurityReview/CompleteSecurityReviewDialog'; import RemoveSecurityReviewDialog from 'features/submissions/components/PublishSecurityReview/RemoveSecurityReviewDialog'; -import { SubmissionRecordWithSecurity } from 'interfaces/useDatasetApi.interface'; +import { SubmissionRecordWithSecurity } from 'interfaces/useSubmissionsApi.interface'; import { useState } from 'react'; export interface IPublishSecurityReviewButtonProps { diff --git a/app/src/features/submissions/components/SubmissionHeaderSecurityStatus.tsx b/app/src/features/submissions/components/SubmissionHeaderSecurityStatus.tsx index 72f59aae0..f4b203c12 100644 --- a/app/src/features/submissions/components/SubmissionHeaderSecurityStatus.tsx +++ b/app/src/features/submissions/components/SubmissionHeaderSecurityStatus.tsx @@ -2,7 +2,8 @@ import { mdiClipboardTextOutline, mdiLock, mdiLockAlertOutline, mdiLockOpenVaria import Icon from '@mdi/react'; import Typography from '@mui/material/Typography'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; -import { SECURITY_APPLIED_STATUS, SubmissionRecordWithSecurity } from 'interfaces/useDatasetApi.interface'; +import { SECURITY_APPLIED_STATUS } from 'interfaces/useDatasetApi.interface'; +import { SubmissionRecordWithSecurity } from 'interfaces/useSubmissionsApi.interface'; import { getFormattedDate } from 'utils/Utils'; export interface ISubmissionHeaderSecurityStatusProps { diff --git a/app/src/hooks/api/useSubmissionsApi.ts b/app/src/hooks/api/useSubmissionsApi.ts index 228040f6c..856f75541 100644 --- a/app/src/hooks/api/useSubmissionsApi.ts +++ b/app/src/hooks/api/useSubmissionsApi.ts @@ -1,6 +1,11 @@ import { AxiosInstance } from 'axios'; -import { SECURITY_APPLIED_STATUS, SubmissionRecord } from 'interfaces/useDatasetApi.interface'; -import { IGetSubmissionResponse, IListSubmissionsResponse, ISubmission } from 'interfaces/useSubmissionsApi.interface'; +import { SECURITY_APPLIED_STATUS } from 'interfaces/useDatasetApi.interface'; +import { + IGetSubmissionResponse, + IListSubmissionsResponse, + ISubmission, + SubmissionRecordWithRootFeature +} from 'interfaces/useSubmissionsApi.interface'; /** * Returns a set of supported CRUD api methods submissions. @@ -86,13 +91,9 @@ const useSubmissionsApi = (axios: AxiosInstance) => { /** * Fetch all submissions that have not completed security review. * - * @return {*} {(Promise< - * (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - * >)} + * @return {*} {Promise} */ - const getUnreviewedSubmissions = async (): Promise< - (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - > => { + const getUnreviewedSubmissions = async (): Promise => { const { data } = await axios.get(`api/administrative/submission/unreviewed`); return data; @@ -101,13 +102,9 @@ const useSubmissionsApi = (axios: AxiosInstance) => { /** * Fetch all submissions that have completed security review. * - * @return {*} {(Promise< - * (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - * >)} + * @return {*} {Promise} */ - const getReviewedSubmissions = async (): Promise< - (SubmissionRecord & { feature_type_id: number; feature_type: string })[] - > => { + const getReviewedSubmissions = async (): Promise => { const { data } = await axios.get(`api/administrative/submission/reviewed`); return data; diff --git a/app/src/interfaces/useDatasetApi.interface.ts b/app/src/interfaces/useDatasetApi.interface.ts index 13600e6c9..08989f15f 100644 --- a/app/src/interfaces/useDatasetApi.interface.ts +++ b/app/src/interfaces/useDatasetApi.interface.ts @@ -59,35 +59,6 @@ export interface IDatasetForReview { keywords: string[]; } -export type SubmissionRecord = { - submission_id: number; - uuid: string; - security_review_timestamp: string | null; - source_system: string; - name: string; - description: string; - create_date: string; - create_user: number; - update_date: string | null; - update_user: number | null; - revision_count: number; -}; - -export type SubmissionRecordWithSecurity = { - submission_id: number; - uuid: string; - security_review_timestamp: string | null; - source_system: string; - name: string; - description: string; - create_date: string; - create_user: number; - update_date: string | null; - update_user: number | null; - revision_count: number; - security: SECURITY_APPLIED_STATUS; -}; - export interface ISubmission { submission_id: number; uuid: string; diff --git a/app/src/interfaces/useSubmissionsApi.interface.ts b/app/src/interfaces/useSubmissionsApi.interface.ts index 3b122877f..9c2ddfc37 100644 --- a/app/src/interfaces/useSubmissionsApi.interface.ts +++ b/app/src/interfaces/useSubmissionsApi.interface.ts @@ -1,4 +1,4 @@ -import { SECURITY_APPLIED_STATUS, SubmissionRecordWithSecurity } from './useDatasetApi.interface'; +import { SECURITY_APPLIED_STATUS } from './useDatasetApi.interface'; export type IListSubmissionsResponse = Array<{ submission_id: number; @@ -20,6 +20,29 @@ export type IListSubmissionsResponse = Array<{ /** NET-NEW INTERFACES FOR UPDATED SCHEMA **/ +export type SubmissionRecord = { + submission_id: number; + uuid: string; + security_review_timestamp: string | null; + source_system: string; + name: string; + description: string; + create_date: string; + create_user: number; + update_date: string | null; + update_user: number | null; + revision_count: number; +}; + +export type SubmissionRecordWithSecurity = SubmissionRecord & { + security: SECURITY_APPLIED_STATUS; +}; + +export type SubmissionRecordWithRootFeature = SubmissionRecord & { + feature_type_id: number; + feature_type: string; +}; + export interface ISubmission { submission_id: number; submission_feature_id: number;