diff --git a/api/src/openapi/root-api-doc.ts b/api/src/openapi/root-api-doc.ts index 07a7ea875..f4865ef6d 100644 --- a/api/src/openapi/root-api-doc.ts +++ b/api/src/openapi/root-api-doc.ts @@ -168,6 +168,28 @@ export const rootAPIDoc = { } }, additionalProperties: false + }, + feature: { + type: 'object', + required: ['submission_feature_id', 'submission_id', 'feature_type', 'data', 'parent_submission_feature_id'], + properties: { + submission_feature_id: { + type: 'number' + }, + submission_id: { + type: 'number' + }, + feature_type: { + type: 'string' + }, + data: { + type: 'object' + }, + parent_submission_feature_id: { + type: 'number', + nullable: true + } + } } } } diff --git a/api/src/paths/dataset/{datasetId}/index.ts b/api/src/paths/dataset/{datasetId}/index.ts deleted file mode 100644 index 81fe23890..000000000 --- a/api/src/paths/dataset/{datasetId}/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { getAPIUserDBConnection, getDBConnection } from '../../../database/db'; -import { defaultErrorResponses } from '../../../openapi/schemas/http-responses'; -import { DatasetService } from '../../../services/dataset-service'; -import { getLogger } from '../../../utils/logger'; - -const defaultLog = getLogger('paths/dataset/{datasetId}'); - -export const GET: Operation = [getDatasetInformation()]; - -GET.apiDoc = { - description: 'retrieves dataset data from the submission table', - tags: ['eml'], - security: [ - { - OptionalBearer: [] - } - ], - parameters: [ - { - description: 'dataset uuid', - in: 'path', - name: 'datasetId', - schema: { - type: 'string', - format: 'uuid' - }, - required: true - } - ], - responses: { - 200: { - description: 'Dataset metadata response object.', - content: { - 'application/json': { - schema: { - type: 'object' - //TODO: add schema - } - } - } - }, - ...defaultErrorResponses - } -}; - -/** - * Retrieves dataset data from the submission table. - * - * @returns {RequestHandler} - */ -export function getDatasetInformation(): RequestHandler { - return async (req, res) => { - const connection = req['keycloak_token'] ? getDBConnection(req['keycloak_token']) : getAPIUserDBConnection(); - - const datasetId = String(req.params.datasetId); - - try { - await connection.open(); - - const datasetService = new DatasetService(connection); - - const result = await datasetService.getDatasetByDatasetUUID(datasetId); - - await connection.commit(); - - res.status(200).json(result); - } catch (error) { - defaultLog.error({ label: 'getMetadataByDatasetId', message: 'error', error }); - await connection.rollback(); - throw error; - } finally { - connection.release(); - } - }; -} diff --git a/api/src/paths/dataset/intake.ts b/api/src/paths/submission/intake.ts similarity index 83% rename from api/src/paths/dataset/intake.ts rename to api/src/paths/submission/intake.ts index dbc6907ad..8a2eb9d1d 100644 --- a/api/src/paths/dataset/intake.ts +++ b/api/src/paths/submission/intake.ts @@ -10,7 +10,11 @@ import { ValidationService } from '../../services/validation-service'; import { getKeycloakSource } from '../../utils/keycloak-utils'; import { getLogger } from '../../utils/logger'; -const defaultLog = getLogger('paths/dataset/intake'); +const defaultLog = getLogger('paths/submission/intake'); + +/* +TODO: UPDATED PATH TO SUBMISSION/INTAKE NEED TO UPDATE SIMS TO USE THIS PATH +*/ export const POST: Operation = [ authorizeRequestHandler(() => { @@ -23,12 +27,12 @@ export const POST: Operation = [ ] }; }), - datasetIntake() + submissionIntake() ]; POST.apiDoc = { - description: 'Submit dataset to BioHub', - tags: ['dataset'], + description: 'Submit submission to BioHub', + tags: ['submission'], security: [ { Bearer: [] @@ -48,7 +52,7 @@ POST.apiDoc = { }, type: { type: 'string', - enum: ['dataset'] + enum: ['submission'] }, properties: { title: 'Dataset properties', @@ -89,7 +93,7 @@ POST.apiDoc = { } }; -export function datasetIntake(): RequestHandler { +export function submissionIntake(): RequestHandler { return async (req, res) => { const sourceSystem = getKeycloakSource(req['keycloak_token']); @@ -99,7 +103,7 @@ export function datasetIntake(): RequestHandler { ]); } - const dataset = { + const submission = { ...req.body, properties: { ...req.body.properties, additionalInformation: req.body.properties.additionalInformation } }; @@ -113,21 +117,21 @@ export function datasetIntake(): RequestHandler { const submissionService = new SubmissionService(connection); const validationService = new ValidationService(connection); - // validate the dataset submission - if (!(await validationService.validateDatasetSubmission(dataset))) { - throw new HTTP400('Invalid dataset submission'); + // validate the submission submission + if (!(await validationService.validateDatasetSubmission(submission))) { + throw new HTTP400('Invalid submission submission'); } // insert the submission record const response = await submissionService.insertSubmissionRecordWithPotentialConflict(id); // insert each submission feature record - await submissionService.insertSubmissionFeatureRecords(response.submission_id, dataset.features); + await submissionService.insertSubmissionFeatureRecords(response.submission_id, submission.features); await connection.commit(); res.status(200).json(response); } catch (error) { - defaultLog.error({ label: 'datasetIntake', message: 'error', error }); + defaultLog.error({ label: 'submissionIntake', message: 'error', error }); await connection.rollback(); throw error; } finally { diff --git a/api/src/paths/submission/{submissionUUID}/index.ts b/api/src/paths/submission/{submissionUUID}/index.ts new file mode 100644 index 000000000..2b93594c2 --- /dev/null +++ b/api/src/paths/submission/{submissionUUID}/index.ts @@ -0,0 +1,126 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { getAPIUserDBConnection, getDBConnection } from '../../../database/db'; +import { defaultErrorResponses } from '../../../openapi/schemas/http-responses'; +import { SubmissionService } from '../../../services/submission-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/submission/{submissionUUID}}'); + +export const GET: Operation = [getSubmissionInformation()]; + +GET.apiDoc = { + description: 'retrieves submission data from the submission table', + tags: ['eml'], + security: [ + { + OptionalBearer: [] + } + ], + parameters: [ + { + description: 'submission uuid', + in: 'path', + name: 'submissionUUID', + schema: { + type: 'string', + format: 'uuid' + }, + required: true + } + ], + responses: { + 200: { + description: 'Dataset metadata response object.', + content: { + 'application/json': { + schema: { + type: 'object', + required: ['submission', 'features'], + properties: { + submission: { + type: 'object', + required: ['submission_id', 'uuid', 'security_review_timestamp'], + properties: { + submission_id: { + type: 'number' + }, + uuid: { + type: 'string', + format: 'uuid' + }, + security_review_timestamp: { + type: 'string', + format: 'date-time', + nullable: true + } + } + }, + features: { + required: ['dataset', 'sampleSites', 'animals', 'observations'], + properties: { + dataset: { + type: 'array', + items: { + $ref: '#/components/schemas/feature' + } + }, + sampleSites: { + type: 'array', + items: { + $ref: '#/components/schemas/feature' + } + }, + animals: { + type: 'array', + items: { + $ref: '#/components/schemas/feature' + } + }, + observations: { + type: 'array', + items: { + $ref: '#/components/schemas/feature' + } + } + } + } + } + } + } + } + }, + ...defaultErrorResponses + } +}; + +/** + * Retrieves submission data from the submission table. + * + * @returns {RequestHandler} + */ +export function getSubmissionInformation(): RequestHandler { + return async (req, res) => { + const connection = req['keycloak_token'] ? getDBConnection(req['keycloak_token']) : getAPIUserDBConnection(); + + const submissionUUID = String(req.params.submissionUUID); + + try { + await connection.open(); + + const submissionService = new SubmissionService(connection); + + const result = await submissionService.getSubmissionAndFeaturesBySubmissionUUID(submissionUUID); + + await connection.commit(); + + res.status(200).json(result); + } catch (error) { + defaultLog.error({ label: 'getSubmissionInformation', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/repositories/submission-repository.ts b/api/src/repositories/submission-repository.ts index 113808829..f512b9009 100644 --- a/api/src/repositories/submission-repository.ts +++ b/api/src/repositories/submission-repository.ts @@ -76,6 +76,22 @@ export interface ISubmissionRecord { revision_count?: string; } +export interface ISubmissionFeatureRecord { + submission_feature_id?: number; + submission_id: number; + feature_type_id: number; + data: any; // TODO: IFeatureSubmission; + feature_type?: string; + parent_submission_feature_id?: number; + record_effective_date?: string; + record_end_date?: string; + create_date?: string; + create_user?: string; + update_date?: string; + update_user?: string; + revision_count?: string; +} + export interface ISubmissionRecordWithSpatial { id: string; source: Record; @@ -1126,11 +1142,11 @@ export class SubmissionRepository extends BaseRepository { */ async getUnreviewedSubmissionsForAdmins(): Promise { const sqlStatement = SQL` - SELECT + SELECT * - FROM + FROM submission - WHERE + WHERE submission.security_review_timestamp is null; `; @@ -1147,15 +1163,46 @@ export class SubmissionRepository extends BaseRepository { */ async getReviewedSubmissionsForAdmins(): Promise { const sqlStatement = SQL` - SELECT + SELECT * - FROM + FROM submission - WHERE + WHERE submission.security_review_timestamp is not null; `; const response = await this.connection.sql(sqlStatement, SubmissionRecord); + return response.rows; + } + + /* + * Fetch a submission from uuid. + * + * @param {number} submissionId + * @return {*} {Promise} + * @memberof SubmissionRepository + */ + async getSubmissionFeaturesBySubmissionId(submissionId: number): Promise { + const sqlStatement = SQL` + SELECT + sf.submission_feature_id, + sf.submission_id, + (SELECT name FROM feature_type WHERE feature_type_id = sf.feature_type_id) AS feature_type, + sf.data, + sf.parent_submission_feature_id + FROM + submission_feature sf + WHERE + submission_id = ${submissionId}; + `; + const response = await this.connection.sql(sqlStatement); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to get submission feature record', [ + 'SubmissionRepository->getSubmissionFeaturesBySubmissionId', + 'rowCount was null or undefined, expected rowCount != 0' + ]); + } return response.rows; } diff --git a/api/src/services/dataset-service.ts b/api/src/services/dataset-service.ts index d15ccaa13..1aaaccaf0 100644 --- a/api/src/services/dataset-service.ts +++ b/api/src/services/dataset-service.ts @@ -10,18 +10,4 @@ export class DatasetService extends DBService { this.submissionRepository = new SubmissionRepository(connection); } - - /** - * Retrieves dataset data from the submission table. - * - * @param {string} datasetUUID - * @return {*} {Promise} - * @memberof DatasetService - */ - async getDatasetByDatasetUUID(datasetUUID: string): Promise { - const submission = this.submissionRepository.getSubmissionByUUID(datasetUUID); - console.log('submission', submission); - - return submission; - } } diff --git a/api/src/services/submission-service.ts b/api/src/services/submission-service.ts index aacde9dc3..c36bf718b 100644 --- a/api/src/services/submission-service.ts +++ b/api/src/services/submission-service.ts @@ -554,4 +554,47 @@ export class SubmissionService extends DBService { async getReviewedSubmissionsForAdmins(): Promise { return this.submissionRepository.getReviewedSubmissionsForAdmins(); } + + /* + * Retrieves submission data from the submission table. + * + * @param {string} submissionUUID + * @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}`); + } + + const features = await this.submissionRepository.getSubmissionFeaturesBySubmissionId(submission.submission_id); + + const dataset = []; + const sampleSites = []; + const animals = []; + const observations = []; + + for (const feature of features) { + const featureType = feature.feature_type; + + switch (featureType) { + case 'dataset': + dataset.push(feature); + break; + case 'sample_site': + sampleSites.push(feature); + break; + case 'animal': + animals.push(feature); + break; + case 'observation': + observations.push(feature); + break; + } + } + + return { submission, features: { dataset, sampleSites, animals, observations } }; + } } diff --git a/app/src/AppRouter.tsx b/app/src/AppRouter.tsx index 454ea37e0..9ef2071fd 100644 --- a/app/src/AppRouter.tsx +++ b/app/src/AppRouter.tsx @@ -69,7 +69,7 @@ const AppRouter: React.FC = () => { - + { + const { title, subTitle, breadCrumb, buttonJSX } = props; + + return ( + + + {breadCrumb} + + + + {title} + + {subTitle} + + {buttonJSX} + + + + ); +}; + +export default BaseHeader; diff --git a/app/src/components/layout/header/Header.tsx b/app/src/components/layout/header/Header.tsx index 4ba90a7d4..8c4d6119c 100644 --- a/app/src/components/layout/header/Header.tsx +++ b/app/src/components/layout/header/Header.tsx @@ -151,8 +151,10 @@ const Header: React.FC = () => { - - Submission 1 + + Submission test page diff --git a/app/src/contexts/submissionContext.tsx b/app/src/contexts/submissionContext.tsx new file mode 100644 index 000000000..a934b2613 --- /dev/null +++ b/app/src/contexts/submissionContext.tsx @@ -0,0 +1,64 @@ +import { useApi } from 'hooks/useApi'; +import useDataLoader, { DataLoader } from 'hooks/useDataLoader'; +import { IGetSubmissionResponse } from 'interfaces/useDatasetApi.interface'; +import React, { useEffect, useMemo } from 'react'; +import { useParams } from 'react-router'; + +export interface ISubmissionContext { + /** + * The Data Loader used to load submission data + * + * @type {DataLoader<[submissionUUID: string], IGetSubmissionResponse, unknown>} + * @memberof ISubmissionContext + */ + submissionDataLoader: DataLoader<[submissionUUID: string], IGetSubmissionResponse, unknown>; + /** + * The submission UUID + * + * @type {string} + * @memberof ISubmissionContext + */ + submissionUUID: string; +} + +export const SubmissionContext = React.createContext({ + submissionDataLoader: {} as DataLoader<[submissionUUID: string], IGetSubmissionResponse, unknown>, + submissionUUID: '' +}); + +export const SubmissionContextProvider: React.FC = (props) => { + const biohubApi = useApi(); + const submissionDataLoader = useDataLoader(biohubApi.submissions.getSubmission); + + const urlParams: Record = useParams(); + + if (!urlParams['submission_uuid']) { + throw new Error( + "The submission UUID found in SubmissionContextProvider was invalid. Does your current React route provide an 'id' parameter?" + ); + } + + const submissionUUID = urlParams['submission_uuid'] as string; + + submissionDataLoader.load(submissionUUID); + + /** + * Refreshes the current submission object whenever the current submission UUID changes from the currently loaded submission. + */ + useEffect(() => { + if (submissionUUID && submissionUUID !== submissionDataLoader.data?.submission.uuid) { + submissionDataLoader.refresh(submissionUUID); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [submissionUUID]); + + const surveyContext: ISubmissionContext = useMemo(() => { + return { + submissionDataLoader, + submissionUUID + }; + }, [submissionDataLoader, submissionUUID]); + + return {props.children}; +}; diff --git a/app/src/features/admin/dashboard/AdminDashboardRouter.tsx b/app/src/features/admin/dashboard/AdminDashboardRouter.tsx index 56a426845..1c187e5cf 100644 --- a/app/src/features/admin/dashboard/AdminDashboardRouter.tsx +++ b/app/src/features/admin/dashboard/AdminDashboardRouter.tsx @@ -1,3 +1,5 @@ +import { SubmissionContextProvider } from 'contexts/submissionContext'; +import AdminSubmissionPage from 'features/submissions/AdminSubmissionPage'; import { Switch } from 'react-router'; import RouteWithTitle from 'utils/RouteWithTitle'; import { getTitle } from 'utils/Utils'; @@ -14,6 +16,12 @@ const AdminDashboardRouter: React.FC = () => { + + + + + + ); }; diff --git a/app/src/features/datasets/DatasetPage.tsx b/app/src/features/datasets/DatasetPage.tsx index d2d2b605d..aaa5910e9 100644 --- a/app/src/features/datasets/DatasetPage.tsx +++ b/app/src/features/datasets/DatasetPage.tsx @@ -4,9 +4,6 @@ import Container from '@mui/material/Container'; import Paper from '@mui/material/Paper'; import { makeStyles } from '@mui/styles'; import { ActionToolbar } from 'components/toolbar/ActionToolbars'; -import { useApi } from 'hooks/useApi'; -import useDataLoader from 'hooks/useDataLoader'; -import { useParams } from 'react-router'; const useStyles = makeStyles((theme: Theme) => ({ datasetTitleContainer: { @@ -27,14 +24,14 @@ const useStyles = makeStyles((theme: Theme) => ({ const DatasetPage: React.FC = () => { const classes = useStyles(); - const biohubApi = useApi(); - const urlParams = useParams(); + // const biohubApi = useApi(); + // const urlParams = useParams(); // const dialogContext = useContext(DialogContext); // const history = useHistory(); - const datasetId = urlParams['id']; + // const datasetId = urlParams['id']; - const datasetDataLoader = useDataLoader(() => biohubApi.dataset.getDataset(datasetId)); + // const datasetDataLoader = useDataLoader(() => biohubApi.dataset.getDataset(datasetId)); // const templateDataLoader = useDataLoader(() => biohubApi.dataset.getHandleBarsTemplateByDatasetId(datasetId)); // const fileDataLoader = useDataLoader((searchBoundary: Feature, searchType: string[], searchZoom: number) => @@ -64,8 +61,8 @@ const DatasetPage: React.FC = () => { // }; // }); - datasetDataLoader.load(); - console.log('datasetDataLoader.data', datasetDataLoader.data); + // datasetDataLoader.load(); + // console.log('datasetDataLoader.data', datasetDataLoader.data); // // templateDataLoader.load(); // const mapDataLoader = useDataLoader((searchBoundary: Feature, searchType: string[], searchZoom: number) => diff --git a/app/src/features/submissions/AdminSubmissionPage.tsx b/app/src/features/submissions/AdminSubmissionPage.tsx new file mode 100644 index 000000000..24d7b0dd1 --- /dev/null +++ b/app/src/features/submissions/AdminSubmissionPage.tsx @@ -0,0 +1,72 @@ +import { Theme } from '@mui/material'; +import Box from '@mui/material/Box'; +import Container from '@mui/material/Container'; +import Paper from '@mui/material/Paper'; +import { makeStyles } from '@mui/styles'; +import { ActionToolbar } from 'components/toolbar/ActionToolbars'; +import SubmissionHeader from './components/SubmissionHeader'; + +const useStyles = makeStyles((theme: Theme) => ({ + datasetTitleContainer: { + paddingBottom: theme.spacing(5), + background: '#f7f8fa', + '& h1': { + marginTop: '-4px' + } + }, + datasetDetailsLabel: { + borderBottom: '1pt solid #dadada' + }, + datasetDetailsContainer: {}, + datasetMapContainer: { + minHeight: '400px' + } +})); + +const AdminSubmissionPage: React.FC = () => { + const classes = useStyles(); + + return ( + + + + + + + + + {/* */} + + + {/* */} + + + + + {/* */} + + + {/* */} + + + + + ); +}; + +export default AdminSubmissionPage; diff --git a/app/src/features/submissions/components/SubmissionHeader.tsx b/app/src/features/submissions/components/SubmissionHeader.tsx new file mode 100644 index 000000000..b5ef5a8d9 --- /dev/null +++ b/app/src/features/submissions/components/SubmissionHeader.tsx @@ -0,0 +1,137 @@ +import { mdiChevronDown, mdiCog, mdiLock, mdiLockOpen, mdiPencil } 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 React, { useContext, useState } from 'react'; +import { useHistory } from 'react-router'; +import { Link as RouterLink } from 'react-router-dom'; + +/** + * Submission header for a single-submission view. + * + * @return {*} + */ +const SubmissionHeader = () => { + const history = useHistory(); + const submissionContext = useContext(SubmissionContext); + + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + + const submissionUUID = submissionContext.submissionUUID; + const submissionDataLoader = submissionContext.submissionDataLoader; + + const submissionData = submissionDataLoader.data; + console.log('submissionData', submissionData); + + if (!submissionDataLoader.data) { + return ; + } + + const submission = submissionDataLoader.data?.submission; + const features = submissionDataLoader.data?.features; + console.log('features', features); + const dataset = features?.dataset[0]; + console.log('dataset', dataset); + + return ( + <> + + + SUBMISSION DASHBOARD + + + {dataset?.data.name} | {submissionUUID} + + + } + subTitle={ + + + {submission?.security_review_timestamp ? ( + <> + + + SECURED: {submission?.security_review_timestamp} + + + ) : ( + <> + + + PENDING REVIEW + + + )} + + + } + buttonJSX={ + <> + + + + + + + setMenuAnchorEl(null)}> + history.push('edit')}> + + + + MANAGE SECURITY + + + + } + /> + + ); +}; + +export default SubmissionHeader; diff --git a/app/src/hooks/api/useDatasetApi.ts b/app/src/hooks/api/useDatasetApi.ts index af02498fb..762c00db4 100644 --- a/app/src/hooks/api/useDatasetApi.ts +++ b/app/src/hooks/api/useDatasetApi.ts @@ -48,19 +48,6 @@ const useDatasetApi = (axios: AxiosInstance) => { return data; }; - /** - * Fetch dataset data by datasetUUID. - * - * @param {string} datasetUUID - * @return {*} {Promise} - */ - const getDataset = async (datasetUUID: string): Promise => { - const { data } = await axios.get(`api/dataset/${datasetUUID}`); - console.log('data', data); - - return data; - }; - /** * Fetch dataset artifacts by datasetId. * @@ -112,7 +99,6 @@ const useDatasetApi = (axios: AxiosInstance) => { listAllDatasets, getUnreviewedSubmissions, getDatasetEML, - getDataset, getDatasetArtifacts, getArtifactSignedUrl, getHandleBarsTemplateByDatasetId, diff --git a/app/src/hooks/api/useSubmissionsApi.ts b/app/src/hooks/api/useSubmissionsApi.ts index 1b32f6f8d..3a2c44fe2 100644 --- a/app/src/hooks/api/useSubmissionsApi.ts +++ b/app/src/hooks/api/useSubmissionsApi.ts @@ -71,11 +71,23 @@ const useSubmissionsApi = (axios: AxiosInstance) => { return { mockJson: 'mockValue' }; }; + /** + * Fetch submission data by submissionUUID. + * + * @param {string} submissionUUID + * @return {*} {Promise} //TODO: type + */ + const getSubmission = async (submissionUUID: string): Promise => { + const { data } = await axios.get(`api/submission/${submissionUUID}`); + return data; + }; + return { listSubmissions, getSignedUrl, listReviewedSubmissions, - getSubmissionDownloadPackage + getSubmissionDownloadPackage, + getSubmission }; }; diff --git a/app/src/interfaces/useDatasetApi.interface.ts b/app/src/interfaces/useDatasetApi.interface.ts index 945100fdd..16eb4b15f 100644 --- a/app/src/interfaces/useDatasetApi.interface.ts +++ b/app/src/interfaces/useDatasetApi.interface.ts @@ -66,3 +66,25 @@ export interface IUnreviewedSubmission { description: string; create_date: string; } +export interface ISubmission { + submission_id: number; + uuid: string; + security_review_timestamp: string; +} + +export interface IFeature { + submission_feature_id: number; + submission_id: number; + feature_type: string; + data: any; + parent_submission_feature_id: number | null; +} +export interface IGetSubmissionResponse { + submission: ISubmission; + features: { + dataset: IFeature[]; + sampleSites: IFeature[]; + animals: IFeature[]; + observations: IFeature[]; + }; +}