Skip to content

Commit

Permalink
🔧 Adapt metadata to allow multiple values into one value
Browse files Browse the repository at this point in the history
  • Loading branch information
lflangis committed Feb 5, 2024
1 parent d84303e commit e43db3c
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 23 deletions.
18 changes: 8 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"@babel/core": "^7.16.0",
"@dnd-kit/core": "^4.0.3",
"@dnd-kit/sortable": "^5.1.0",
"@ferlab/ui": "^8.0.3",
"@ferlab/ui": "^8.2.2",
"@loadable/component": "^5.15.2",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
"@react-keycloak/core": "^3.2.0",
Expand Down
5 changes: 5 additions & 0 deletions src/components/Cavatica/AnalyzeButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
beginCavaticaAnalyse,
connectCavaticaPassport,
createCavaticaProjet,
startBulkImportJob,
} from 'store/passport/thunks';

interface OwnProps {
Expand Down Expand Up @@ -72,6 +73,7 @@ const CavaticaAnalyzeButton: React.FC<OwnProps> = ({
handleBeginAnalyse={onBeginAnalyse}
handleFilesAndFolders={CavaticaApi.listFilesAndFolders}
cavatica={cavatica}
loading={cavatica.bulkImportData.loading}
setCavaticaBulkImportDataStatus={(status: CAVATICA_ANALYSE_STATUS) => {
dispatch(passportActions.setCavaticaBulkImportDataStatus(status));
}}
Expand All @@ -90,6 +92,9 @@ const CavaticaAnalyzeButton: React.FC<OwnProps> = ({
}),
);
}}
handleImportBulkData={(value) => {
dispatch(startBulkImportJob(value));
}}
dictionary={{
analyseModal: {
copyFiles: intl.get(
Expand Down
12 changes: 8 additions & 4 deletions src/components/utils/NotificationContextHolder/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { globalActions, useGlobals } from 'store/global';
import { notification as antNotification } from 'antd';
import { message as antMessage } from 'antd';
import { message as antMessage, notification as antNotification } from 'antd';
import cx from 'classnames';

import { globalActions, useGlobals } from 'store/global';

import styles from './index.module.scss';

const NotificationContextHolder = () => {
const dispatch = useDispatch();
// notification is local only
const { notification, message, messagesToDestroy } = useGlobals();

useEffect(() => {
if (notification) {
antNotification.open({
...notification,
description: React.createElement('div', {
dangerouslySetInnerHTML: { __html: notification.description },
}),
style: undefined,
onClose: () => {
if (notification.onClose) {
Expand Down
20 changes: 20 additions & 0 deletions src/graphql/files/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ export const SEARCH_FILES_QUERY = gql`
file_name
repository
nb_participants
participants {
hits {
edges {
node {
participant_id
is_proband
biospecimens {
hits {
edges {
node {
sample_type
sample_type
}
}
}
}
}
}
}
}
nb_biospecimens
fhir_document_reference
index {
Expand Down
26 changes: 26 additions & 0 deletions src/graphql/utils/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { toNodes } from './helpers';

describe(`${toNodes.name}()`, () => {
test('should handle edge case', () => {
expect(
toNodes({
hits: {
edges: [],
},
}),
).toEqual([]);
});
test('should return nodes content', () => {
expect(
toNodes({
hits: {
edges: [
{ node: { prop: false } },
{ node: { prop: true, a: 'a' } },
{ node: { prop: true, b: 'b' } },
],
},
}),
).toEqual([{ prop: false }, { prop: true, a: 'a' }, { prop: true, b: 'b' }]);
});
});
3 changes: 3 additions & 0 deletions src/graphql/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { IArrangerResultsTree } from '@ferlab/ui/core/graphql/types';

export const toNodes = (o: IArrangerResultsTree<any>) => o?.hits?.edges?.map((x) => x.node) || [];
3 changes: 3 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ const en = {
},
success: {
title: 'Success',
description: `<div><p>Your files have been copied to: <strong>{destination}</strong</p>
<p>If you have uploaded more than 10000 files in the last 5 minutes, the import may take a little longer.</p>
<a href="{userBaseUrl}" rel="noreferrer" style="color:unset;textDecoration:underline;" target="_blank">Open project in Cavatica</a><div>`,
projects: {
create: 'Project created successfully',
},
Expand Down
45 changes: 45 additions & 0 deletions src/store/passport/thunks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { IFileEntity } from 'graphql/files/models';

import { metadata } from './thunks';

describe(`${metadata.name}()`, () => {
test('should handle edge case', () => {
expect(metadata({} as IFileEntity)).toEqual({});
});
test('should return well-formed metadata', () => {
const file = {
fhir_id: '1038816',
file_id: 'GF_001JWT9N',
participants: {
hits: {
edges: [
{
node: {
participant_id: 'PT_G16VK7FR',
is_proband: false,
biospecimens: { hits: { edges: [{ node: { sample_type: 'RNA' } }] } },
},
},
],
},
},
fhir_document_reference: 'http://localhost:8000/DocumentReference?identifier=GF_001JWT9N',
study: {
study_id: 'SD_8Y99QZJJ',
study_name: 'Pediatric Brain Tumor Atlas: PNOC',
study_code: 'PBTA-PNOC',
},
sequencing_experiment: {
hits: { edges: [{ node: { experiment_strategy: 'RNA-Seq', platform: 'Illumina' } }] },
},
};
expect(metadata(file as IFileEntity)).toEqual({
fhir_document_reference: 'http://localhost:8000/DocumentReference?identifier=GF_001JWT9N',
participants: 'PT_G16VK7FR',
platform: 'Illumina',
experimental_strategy: 'RNA-Seq',
investigation: 'PBTA-PNOC',
sample_type: 'RNA',
});
});
});
97 changes: 96 additions & 1 deletion src/store/passport/thunks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import intl from 'react-intl-universal';
import { FENCE_AUTHENTIFICATION_STATUS } from '@ferlab/ui/core/components/Widgets/AuthorizedStudies';
import { PASSPORT } from '@ferlab/ui/core/components/Widgets/Cavatica';
import {
CAVATICA_TYPE,
ICavaticaTreeNode,
} from '@ferlab/ui/core/components/Widgets/Cavatica/CavaticaAnalyzeModal';
import {
CAVATICA_ANALYSE_STATUS,
PASSPORT_AUTHENTIFICATION_STATUS,
Expand All @@ -12,7 +15,10 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
import { IFileEntity, IFileResultTree } from 'graphql/files/models';
import { SEARCH_FILES_QUERY } from 'graphql/files/queries';
import { hydrateResults } from 'graphql/models';
import { toNodes } from 'graphql/utils/helpers';
import EnvironmentVariables from 'helpers/EnvVariables';
import { isEmpty } from 'lodash';
import intl from 'react-intl-universal';
import { CAVATICA_FILE_BATCH_SIZE } from 'views/DataExploration/utils/constant';

import { FENCE_NAMES } from 'common/fenceTypes';
Expand All @@ -21,14 +27,19 @@ import { CavaticaApi } from 'services/api/cavatica';
import {
ICavaticaBillingGroup,
ICavaticaCreateProjectBody,
ICavaticaDRSImportItem,
ICavaticaProject,
} from 'services/api/cavatica/models';
import { FenceApi } from 'services/api/fence';
import { globalActions } from 'store/global';
import { RootState } from 'store/types';
import { handleThunkApiReponse } from 'store/utils';
import { userHasAccessToFile } from 'utils/dataFiles';
import { chunkIt, keepOnly } from 'utils/helper';

import { makeUniqueWords as unique } from '../../helpers';

const USER_BASE_URL = EnvironmentVariables.configFor('CAVATICA_USER_BASE_URL');
const TEN_MINUTES_IN_MS = 1000 * 60 * 10;

// TODO: Still using the legacy fence authentification, will be changed in the futur for a passport
Expand Down Expand Up @@ -284,3 +295,87 @@ export const beginCavaticaAnalyse = createAsyncThunk<
},
});
});

export const metadata = (f: IFileEntity) => {
if (!f || !Object.keys(f).length) {
return {};
}
const joinUniquely = (l: string[]) => unique(l).join(',');
const seqExp = toNodes(f.sequencing_experiment);
const ps = toNodes(f.participants);
const sps = ps.map((x) => toNodes(x.biospecimens)).flat();
return keepOnly({
fhir_document_reference: f.fhir_document_reference,
participants: joinUniquely(ps.map((x) => x.participant_id)),
platform: joinUniquely(seqExp.map((x) => x.platform)),
experimental_strategy: joinUniquely(seqExp.map((x) => x.experiment_strategy)),
reference_genome: null,
investigation: f.study?.study_code,
sample_id: joinUniquely(sps.map((x) => x.sample_id)),
sample_type: joinUniquely(sps.map((x) => x.sample_type)),
});
};

export const startBulkImportJob = createAsyncThunk<
any,
ICavaticaTreeNode,
{ rejectValue: string; state: RootState }
>('passport/cavatica/bulk/import', async (args, thunkAPI) => {
const { passport } = thunkAPI.getState();

const drsItems: ICavaticaDRSImportItem[] = [];
const type = args.type === CAVATICA_TYPE.PROJECT ? 'project' : 'parent';
passport.cavatica.bulkImportData.authorizedFiles.forEach((file: IFileEntity) => {
if (file.index) {
drsItems.push({
drs_uri: file.index.urls,
name: file.index.file_name,
[type]: args.id,
});
}

drsItems.push({
drs_uri: file.access_urls,
name: file.file_name,
[type]: args.id,
metadata: metadata(file),
});
});

//https://docs.cavatica.org/reference/start-a-bulk-drs-import-job
const MAX_NUMBER_OF_ITEMS_PER_API_CALL = 100;
const chunks: ICavaticaDRSImportItem[][] = chunkIt(drsItems, MAX_NUMBER_OF_ITEMS_PER_API_CALL);

const responses = await Promise.all(
chunks.map((items: ICavaticaDRSImportItem[]) => CavaticaApi.startBulkDrsImportJob({ items })),
);

const error = responses.find((resp) => !!resp.error);
return handleThunkApiReponse({
error: error?.error,
data: true,
reject: thunkAPI.rejectWithValue,
onError: () =>
thunkAPI.dispatch(
globalActions.displayNotification({
type: 'error',
message: intl.get('api.cavatica.error.title'),
description: intl.get('api.cavatica.error.bulk.import'),
}),
),
onSuccess: () =>
thunkAPI.dispatch(
globalActions.displayNotification({
type: 'success',
message: intl.get('api.cavatica.success.title'),
description: intl.get('api.cavatica.success.description', {
destination: args.title,
userBaseUrl: `${USER_BASE_URL}${
args.type === CAVATICA_TYPE.PROJECT ? args.id : args.project!
}`,
}),
duration: 5,
}),
),
});
});
18 changes: 17 additions & 1 deletion src/utils/helper.test.tsx → src/utils/helper.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SortDirection } from '@ferlab/ui/core/graphql/constants';
import { SorterResult } from 'antd/lib/table/interface';

import { formatQuerySortList, getOrderFromAntdValue } from './helper';
import { chunkIt, formatQuerySortList, getOrderFromAntdValue, keepOnly } from './helper';

describe(`${getOrderFromAntdValue.name}()`, () => {
test('should return SortDirection.Asc for "ascend"', () => {
Expand Down Expand Up @@ -41,3 +41,19 @@ describe(`${formatQuerySortList.name}()`, () => {
expect(formatQuerySortList({})).toEqual([]);
});
});

describe(`${chunkIt.name}()`, () => {
it('should chunk and array into chunks', () => {
expect(chunkIt(['a', 'b', 'c'], 2)).toEqual([['a', 'b'], ['c']]);
});
});

describe(`${keepOnly.name}()`, () => {
it('should handle edge case', () => {
expect(keepOnly({ a: null, b: undefined })).toEqual({});
});

it('should keep only wanted entries', () => {
expect(keepOnly({ a: 'a', b: 2 }, ([, v]) => typeof v === 'number')).toEqual({ b: 2 });
});
});
Loading

0 comments on commit e43db3c

Please sign in to comment.