From 973a57cbdcecae98831a92a665fc3c19f90921d4 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Tue, 30 Jan 2024 17:09:56 -0600 Subject: [PATCH 01/21] update package names and add repository --- .github/workflows/release-package.yaml | 35 ++++++++++++++++++++++++++ packages/core/package.json | 8 +++++- packages/frontend/.npmignore | 15 +++-------- packages/frontend/package.json | 7 +++++- packages/sampleCommons/package.json | 7 +++++- packages/tools/package.json | 7 +++++- 6 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/release-package.yaml diff --git a/.github/workflows/release-package.yaml b/.github/workflows/release-package.yaml new file mode 100644 index 00000000..738a754e --- /dev/null +++ b/.github/workflows/release-package.yaml @@ -0,0 +1,35 @@ +#name: Node.js Package +# +#on: +# push: +# branches: +# - develop +# +#jobs: +# build: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v4 +# - uses: actions/setup-node@v3 +# with: +# node-version: 18.15.0 +# - run: npm ci +# - run: npm test +# +# publish-gpr: +# needs: build +# runs-on: ubuntu-latest +# permissions: +# packages: write +# contents: read +# steps: +# - uses: actions/checkout@v4 +# - uses: actions/setup-node@v3.4.1 +# with: +# node-version: 18.15.0 +# registry-url: https://npm.pkg.github.com/ +# - run: npm ci +# - run: npm publish --workspace packages/core +# env: +# NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} +# diff --git a/packages/core/package.json b/packages/core/package.json index 27625f33..709ae6ad 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,5 +1,10 @@ { "name": "@gen3/core", + "repository": { + "type": "git", + "url": "https://github.com/uc-cdis/gen3-frontend-framework.git", + "directory": "packages/core" + }, "version": "0.1.0", "author": "CTDS", "description": "Core module for gen3 frontend. Provides an interface for interacting with the gen3 API and a redux store for managing state.", @@ -58,6 +63,7 @@ "dist" ], "publishConfig": { - "registry": "https://localhost:4873/" + "registry": "https://npm.pkg.github.com" } + } diff --git a/packages/frontend/.npmignore b/packages/frontend/.npmignore index 08831260..a6112db0 100644 --- a/packages/frontend/.npmignore +++ b/packages/frontend/.npmignore @@ -1,12 +1,3 @@ -# General -node_modules -*.tgz -.DS_Store -.vscode -npm-debug.log -yarn-error.log -.env -dump.rdb -*.retry -coverage -.next/cache/* +src/ +tsconfig.* +setupTests.js diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 14371257..8f40f82b 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,5 +1,10 @@ { "name": "@gen3/frontend", + "repository": { + "type": "git", + "url": "https://github.com/uc-cdis/gen3-frontend-framework.git", + "directory": "packages/frontend" + }, "version": "0.20.2", "description": "Gen3 frontend components, content management, and pages", "keywords": [], @@ -152,6 +157,6 @@ "dist" ], "publishConfig": { - "registry": "https://localhost:4873/" + "registry": "https://npm.pkg.github.com/" } } diff --git a/packages/sampleCommons/package.json b/packages/sampleCommons/package.json index 7a4cec4d..bed8852e 100644 --- a/packages/sampleCommons/package.json +++ b/packages/sampleCommons/package.json @@ -1,5 +1,10 @@ { - "name": "@gen3/datacommonsapp", + "name": "@gen3/sampleCommons", + "repository": { + "type": "git", + "url": "https://github.com/uc-cdis/gen3-frontend-framework.git", + "directory": "packages/sampleCommons" + }, "version": "0.1.0", "private": true, "scripts": { diff --git a/packages/tools/package.json b/packages/tools/package.json index 9f6263f6..acc78345 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -1,5 +1,10 @@ { - "name": "@gen3/toolsff", + "name": "@gen3/tools", + "repository": { + "type": "git", + "url": "https://github.com/uc-cdis/gen3-frontend-framework.git", + "directory": "packages/tools" + }, "version": "0.1.1", "description": "tools for processing portal content", "main": "index.js", From 620cfcf41e97ed2c47f2ee27efba6dc6eaddc155 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Mon, 5 Feb 2024 16:30:33 -0600 Subject: [PATCH 02/21] Still working on downloads --- package-lock.json | 120 ++++++++++++++++++ .../src/features/guppy/guppyDownloadSlice.ts | 29 +---- packages/core/src/features/guppy/index.ts | 4 + packages/core/src/features/guppy/types.ts | 18 +++ packages/core/src/index.ts | 4 +- .../DownloadButtons/DownloadButton.tsx | 47 ++----- .../features/CohortBuilder/CohortPanel.tsx | 7 +- .../features/CohortBuilder/DownloadsPanel.tsx | 73 +++++++---- .../features/CohortBuilder/buttonActions.ts | 1 + .../src/features/CohortBuilder/types.ts | 10 +- packages/frontend/src/utils/download.tsx | 11 +- packages/sampleCommons/.env.development | 2 +- 12 files changed, 233 insertions(+), 93 deletions(-) create mode 100644 packages/core/src/features/guppy/types.ts diff --git a/package-lock.json b/package-lock.json index f0d7d26f..7de5bb71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35430,6 +35430,126 @@ "engines": { "node": ">=10" } + }, + "packages/sampleCommons/node_modules/@next/swc-darwin-x64": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz", + "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz", + "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-linux-arm64-musl": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz", + "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-linux-x64-gnu": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz", + "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-linux-x64-musl": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz", + "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz", + "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz", + "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-win32-x64-msvc": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz", + "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/packages/core/src/features/guppy/guppyDownloadSlice.ts b/packages/core/src/features/guppy/guppyDownloadSlice.ts index 6ad77c03..5add29c2 100644 --- a/packages/core/src/features/guppy/guppyDownloadSlice.ts +++ b/packages/core/src/features/guppy/guppyDownloadSlice.ts @@ -1,30 +1,13 @@ import { gen3Api } from '../gen3'; -import { GEN3_API } from '../../constants'; -import { FilterSet } from '../filters/types'; -import { Accessibility } from '../../constants'; -import { convertFilterSetToGqlFilter, GQLFilter } from '../filters'; +import { GEN3_GUPPY_API } from '../../constants'; +import { convertFilterSetToGqlFilter } from '../filters'; +import { GuppyDownloadQueryParams, GuppyDownloadRequestParams } from './types'; interface DownloadRequestStatus { readonly status: string; readonly message: string; } -interface BaseDownloadRequest { - type: string; - accessibility?: Accessibility; - fields?: string[]; - sort?: string[]; - format?: string; -} - -interface DownloadQueryParams extends BaseDownloadRequest { - filters: FilterSet; -} - -interface DownloadRequestParams extends BaseDownloadRequest { - readonly filter: GQLFilter; -} - export const downloadRequestApi = gen3Api.injectEndpoints({ endpoints: (builder) => ({ downloadFromGuppy: builder.query({ @@ -34,13 +17,13 @@ export const downloadRequestApi = gen3Api.injectEndpoints({ accessibility, fields, sort, - }: DownloadQueryParams) => { - const queryBody: DownloadRequestParams = { + }: GuppyDownloadQueryParams) => { + const queryBody: GuppyDownloadRequestParams = { filter: convertFilterSetToGqlFilter(filters), ...{ type, accessibility, fields, sort }, }; return { - url: `${GEN3_API}/guppy/download`, + url: `${GEN3_GUPPY_API}/download`, method: 'POST', queryBody, }; diff --git a/packages/core/src/features/guppy/index.ts b/packages/core/src/features/guppy/index.ts index 5353300e..9a040ee9 100644 --- a/packages/core/src/features/guppy/index.ts +++ b/packages/core/src/features/guppy/index.ts @@ -1,3 +1,7 @@ export * from './guppylApi'; export * from './guppySlice'; export * from './guppyDownloadSlice'; +export * from './utils'; +import { type GuppyDownloadRequestParams, type GuppyDownloadQueryParams } from './types'; + +export { type GuppyDownloadRequestParams, type GuppyDownloadQueryParams }; diff --git a/packages/core/src/features/guppy/types.ts b/packages/core/src/features/guppy/types.ts new file mode 100644 index 00000000..8491b7f8 --- /dev/null +++ b/packages/core/src/features/guppy/types.ts @@ -0,0 +1,18 @@ +import { FilterSet, GQLFilter } from '../filters'; +import { Accessibility } from '../../constants'; + +export interface BaseGuppyDownloadRequest { + type: string; + accessibility?: Accessibility; + fields?: string[]; + sort?: string[]; + format?: string; +} + +export interface GuppyDownloadQueryParams extends BaseGuppyDownloadRequest { + filters: FilterSet; +} + +export interface GuppyDownloadRequestParams extends BaseGuppyDownloadRequest { + readonly filter: GQLFilter; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d915a6b8..e9823eee 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,4 @@ -import { GEN3_API, GEN3_DOMAIN, GEN3_COMMONS_NAME, GEN3_DOWNLOADS_ENDPOINT, GEN3_GUPPY_API } from './constants'; +import { GEN3_API, GEN3_DOMAIN, GEN3_COMMONS_NAME, GEN3_DOWNLOADS_ENDPOINT, GEN3_GUPPY_API, Accessibility } from './constants'; import { type CoreState } from './reducers'; export * from './features/user'; @@ -19,4 +19,4 @@ export * from './features/cohort'; export * from './features/filters'; export * from './features/guppy'; -export { type CoreState, GEN3_COMMONS_NAME, GEN3_DOMAIN, GEN3_API, GEN3_DOWNLOADS_ENDPOINT, GEN3_GUPPY_API }; +export { type CoreState, GEN3_COMMONS_NAME, GEN3_DOMAIN, GEN3_API, GEN3_DOWNLOADS_ENDPOINT, GEN3_GUPPY_API, Accessibility }; diff --git a/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx b/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx index 0acb9703..01cbdf09 100644 --- a/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx +++ b/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx @@ -1,8 +1,8 @@ -import { Button, ButtonProps, Loader, Tooltip } from "@mantine/core"; -import { FiDownload } from "react-icons/fi"; -import download from "../../utils/download"; -import { hideModal, Modals, useCoreDispatch } from "@gen3/core"; -import { Dispatch, SetStateAction, forwardRef } from "react"; +import { Button, ButtonProps, Loader, Tooltip } from '@mantine/core'; +import { FiDownload } from 'react-icons/fi'; +import download from '../../utils/download'; +import { hideModal, Modals, useCoreDispatch } from '@gen3/core'; +import { Dispatch, SetStateAction, forwardRef } from 'react'; /** * Properties for the DownloadButton component. @@ -11,13 +11,10 @@ import { Dispatch, SetStateAction, forwardRef } from "react"; * @property disabled - Whether the button is disabled. * @property inactiveText - The text to display when the button is inactive. * @property activeText - The text to display when the button is active. - * @property filename - The name of the file to download. - * @property size - The size of the download. + * @property params - The parameters to pass to the endpoint. * @property format - The format of the download. * @property fields - The fields to download. - * @property caseFilters - The case filters to download. * @property filters - The filters to download. - * @property extraParams - Any extra parameters to download. * @property method - The method to use for the download. * @property customStyle - Any custom styles to apply to the button. * @property showLoading - Whether to show the loading icon. @@ -35,13 +32,8 @@ interface DownloadButtonProps { disabled?: boolean; inactiveText: string; activeText: string; - filename?: string; - size?: number; format?: string; - fields?: Array; - caseFilters?: Record; - filters?: Record; - extraParams?: Record; + params: Record; method?: string; customStyle?: string; showLoading?: boolean; @@ -91,16 +83,10 @@ export const DownloadButton = forwardRef< { endpoint, disabled = false, - filename, - size = 10000, - format = "JSON", - fields = [], - caseFilters = {}, - filters = {}, inactiveText, activeText, - extraParams, - method = "POST", + params = {}, + method = 'POST', customStyle, setActive, onClick, @@ -132,7 +118,7 @@ export const DownloadButton = forwardRef< className={ customStyle || `text-base-lightest ${ - disabled ? "bg-base" : "bg-primary hover:bg-primary-darker" + disabled ? 'bg-base' : 'bg-primary hover:bg-primary-darker' } ` } loading={showLoading && active} @@ -142,17 +128,6 @@ export const DownloadButton = forwardRef< return; } dispatch(hideModal()); - const params = { - size, - attachment: true, - format, - fields: fields.join(), - case_filters: caseFilters, - filters, - pretty: true, - ...(filename ? { filename } : {}), - ...extraParams, - }; setActive && setActive(true); download({ params, @@ -173,4 +148,4 @@ export const DownloadButton = forwardRef< }, ); -DownloadButton.displayName = "DownloadButton"; +DownloadButton.displayName = 'DownloadButton'; diff --git a/packages/frontend/src/features/CohortBuilder/CohortPanel.tsx b/packages/frontend/src/features/CohortBuilder/CohortPanel.tsx index de9751fc..f6bb71bb 100644 --- a/packages/frontend/src/features/CohortBuilder/CohortPanel.tsx +++ b/packages/frontend/src/features/CohortBuilder/CohortPanel.tsx @@ -34,6 +34,7 @@ import ExplorerTable from './ExplorerTable/ExplorerTable'; import CountsValue from '../../components/counts/CountsValue'; import DownloadsPanel from './DownloadsPanel'; import { AddButtonsArrayToDropdowns, AddButtonsToDropdown } from './utils'; +import { useDeepCompareMemo } from 'use-deep-compare'; const EmptyData = {}; @@ -124,7 +125,7 @@ export const CohortPanel = ({ loginForDownload, }: CohortPanelConfig): JSX.Element => { const index = guppyConfig.dataType; - const fields = getAllFieldsFromFilterConfigs(filters?.tabs ?? []); + const fields = useMemo(() => getAllFieldsFromFilterConfigs(filters?.tabs ?? []), []); const [facetDefinitions, setFacetDefinitions] = useState< Record @@ -261,6 +262,10 @@ export const CohortPanel = ({ dropdowns={dropdownsWithButtons} buttons={actionButtons} loginForDownload={loginForDownload} + index={index} + totalCount={counts ?? 0} + fields={fields} + filters={cohortFilters} /> . ; buttons: DownloadButtonProps[]; loginForDownload?: boolean; + index: string; + totalCount: number; + fields: string[]; + filters: FilterSet; } const DownloadsPanel = ({ dropdowns, buttons, loginForDownload, + index, + totalCount, + fields, + filters, }: DownloadsPanelProps): JSX.Element => { const isUserLoggedIn = useIsUserLoggedIn(); - const loginRequired = loginForDownload ? loginForDownload : false; - let dropdownsToRender = dropdowns; - - if (loginRequired && !isUserLoggedIn) { - dropdownsToRender = Object.entries(dropdowns ?? {}).reduce( - (acc, [key, dropdown]) => { - return { - ...acc, - [key]: { - ...dropdown, - title: `${dropdown.title} (Login Required)`, - buttons: dropdown.buttons.map((button) => ({ - ...button, - title: `${button.title} (Login Required)`, - enabled: false, - })), - }, - }; - }, - {}, - ); - } + const dropdownsToRender = useDeepCompareMemo(() => { + + let dropdownsToRender = dropdowns; + + if (loginRequired && !isUserLoggedIn) { + dropdownsToRender = Object.entries(dropdowns ?? {}).reduce( + (acc, [key, dropdown]) => { + return { + ...acc, + [key]: { + ...dropdown, + title: `${dropdown.title} (Login Required)`, + buttons: dropdown.buttons.map((button) => ({ + ...button, + title: `${button.title} (Login Required)`, + enabled: false, + })), + }, + }; + }, + {}, + ); + } + return dropdownsToRender; + }, [dropdowns, isUserLoggedIn, loginRequired]); return dropdowns ? (
+ + {Object.values(dropdownsToRender).map((dropdown) => ( ))} {buttons.map((button) => ( ))} +
) : ( - <> + ); }; diff --git a/packages/frontend/src/features/CohortBuilder/buttonActions.ts b/packages/frontend/src/features/CohortBuilder/buttonActions.ts index e69de29b..868b914e 100644 --- a/packages/frontend/src/features/CohortBuilder/buttonActions.ts +++ b/packages/frontend/src/features/CohortBuilder/buttonActions.ts @@ -0,0 +1 @@ +import { GEN3_GUPPY_API, useCoreDispatch, GuppyDownloadRequestParams, Accessibility } from '@gen3/core'; diff --git a/packages/frontend/src/features/CohortBuilder/types.ts b/packages/frontend/src/features/CohortBuilder/types.ts index 2e616264..b727eff2 100644 --- a/packages/frontend/src/features/CohortBuilder/types.ts +++ b/packages/frontend/src/features/CohortBuilder/types.ts @@ -1,7 +1,7 @@ // set of interfaces which follows the current explorer configuration -import { SummaryChart } from '../../components/charts/types'; +import { SummaryChart } from '../../components/charts'; import { SummaryTable } from './ExplorerTable/types'; import { FieldToName } from '../../components/facets/types'; import { DownloadButtonProps } from '../../components/Buttons/DropdownButtons'; @@ -54,3 +54,11 @@ export interface CohortBuilderConfiguration { export interface CohortConfig { tabs: TabConfig[]; } + +export enum DownloadFileFormats { + JSON = 'JSON', + CSV = 'CSV', + TSV = 'TSV', + DATA = 'DATA', + UNDEFINED = 'UNDEFINED', +} diff --git a/packages/frontend/src/utils/download.tsx b/packages/frontend/src/utils/download.tsx index fbc8713d..6db5d134 100644 --- a/packages/frontend/src/utils/download.tsx +++ b/packages/frontend/src/utils/download.tsx @@ -1,4 +1,3 @@ -'use client'; import Cookies from 'universal-cookie'; import { CoreDispatch, Modals, showModal } from '@gen3/core'; @@ -6,7 +5,6 @@ import { Button } from '@mantine/core'; import { cleanNotifications, showNotification } from '@mantine/notifications'; import { includes, isPlainObject, reduce, uniqueId } from 'lodash'; import { RiCloseCircleLine as CloseIcon } from 'react-icons/ri'; -import urlJoin from 'url-join'; const hashString = (s: string) => s.split('').reduce((acc, c) => (acc << 5) - acc + c.charCodeAt(0), 0); @@ -32,7 +30,7 @@ const customKeys = ['expand', 'fields', 'facets']; const processParamObj = (key: string, value: any) => includes(customKeys, key) ? [].concat(value).join() : value; -const DownloadNotification = ({ onClick }: { onClick: () => void }) => { +export const DownloadNotification = ({ onClick }: { onClick: () => void }) => { return (

Download preparation in progress. Please wait...

@@ -84,7 +82,7 @@ const SlowDownloadNotification = ({ onClick }: { onClick: () => void }) => ( /** * Trigger a download by attaching an iFrame to the document with the parameters of the request as fields in a form - * @param endpoint - endpoint to be attached with GDC AUTH API + * @param endpoint - endpoint to be attached with * @param params - body to be attached with post request * @param method - Request Method: GET, PUT, POST * @param dispatch - dispatch send from the parent component to dispatch Modals @@ -92,6 +90,7 @@ const SlowDownloadNotification = ({ onClick }: { onClick: () => void }) => ( * @param Modal400 - Modal for 400 error * @param Modal403 - Modal for 403 error * @param customErrorMessage - custom message to be passed for 400 errors + * @param hideNotification - hide the notification */ const download = async ({ @@ -121,6 +120,7 @@ const download = async ({ to remove the cookie when the response is received (aka the download starts). This will only work on when the FE and BE are on the same domain. */ + const downloadToken = uniqueId(`${+new Date()}-`); const cookieKey = navigator.cookieEnabled ? Math.abs(hashString(JSON.stringify(params) + downloadToken)).toString(16) @@ -168,6 +168,7 @@ const download = async ({ downloadCookieKey: cookieKey, downloadCookiePath: '/', attachment: true, + ...(params.filename !== undefined? { download: params.filename } : {}), }, (result, value, key) => { const paramValue = processParamObj(key, value); @@ -224,7 +225,7 @@ const download = async ({ dispatch(showModal({ modal: Modal400, message: errorMessage })); } else if ( errorMessage === - 'Your token is invalid or expired. Please get a new token from GDC Data Portal.' + 'Your token is invalid or expired. Please get a new token.' ) { dispatch(showModal({ modal: Modal403, message: errorMessage })); } else { diff --git a/packages/sampleCommons/.env.development b/packages/sampleCommons/.env.development index 79ff1317..d8e3c74f 100644 --- a/packages/sampleCommons/.env.development +++ b/packages/sampleCommons/.env.development @@ -1,4 +1,4 @@ -GEN3_COMMONS_NAME=gen3 +GEN3_COMMONS_NAME=bih NEXT_PUBLIC_GEN3_API=https://localhost:3010 NEXT_PUBLIC_GEN3_DOMAIN=https://localhost:3010 #NEXT_PUBLIC_GEN3_GUPPY_API=https://bih.data-commons.org/guppy From 80f75b03c950ead0afb03af04f3a4534355f27d2 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Mon, 5 Feb 2024 16:45:22 -0600 Subject: [PATCH 03/21] add initial GuppyDownload --- packages/core/src/features/guppy/utils.ts | 65 +++++++++++++++++++ .../src/features/CohortBuilder/Downloads.tsx | 46 +++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 packages/core/src/features/guppy/utils.ts create mode 100644 packages/frontend/src/features/CohortBuilder/Downloads.tsx diff --git a/packages/core/src/features/guppy/utils.ts b/packages/core/src/features/guppy/utils.ts new file mode 100644 index 00000000..4ab59278 --- /dev/null +++ b/packages/core/src/features/guppy/utils.ts @@ -0,0 +1,65 @@ + +import { GuppyDownloadQueryParams } from './types'; +import { GEN3_GUPPY_API } from '../../constants'; + +interface GuppyFileDownloadRequestParams extends GuppyDownloadQueryParams { + readonly format: string; + readonly filename: string; + readonly total: number; +} + +type DownloadOptions = { + method: 'GET' | 'POST'; + parameters?: GuppyFileDownloadRequestParams; + requestHeaders?: Record; + onStart?: () => void; + onEnd?: () => void; + onError?: (error: Error) => void; +}; + +export const downloadFromGuppy = ({ + parameters, + method = 'POST', + requestHeaders = {}, + onStart = () => null, + onEnd = () => null, + onError = () => null, + }: DownloadOptions) => { + if (onStart) { + onStart(); + } + const url = new URL(GEN3_GUPPY_API + '/download'); + if (parameters) { + url.search = new URLSearchParams(parameters as any).toString(); + } + fetch(url.toString(), { + method, + headers: { + 'Content-Type': 'application/json', + ...requestHeaders, + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(response.statusText); + } + return response.blob(); + }) + .then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = parameters?.filename || 'download'; + document.body.appendChild(a); + a.click(); + a.remove(); + if (onEnd) { + onEnd(); + } + }) + .catch((error) => { + if (onError) { + onError(error); + } + }); +}; diff --git a/packages/frontend/src/features/CohortBuilder/Downloads.tsx b/packages/frontend/src/features/CohortBuilder/Downloads.tsx new file mode 100644 index 00000000..96f438f7 --- /dev/null +++ b/packages/frontend/src/features/CohortBuilder/Downloads.tsx @@ -0,0 +1,46 @@ + + +import { DownloadButton } from '../../components/DownloadButtons/DownloadButton'; +import { GEN3_GUPPY_API, useCoreDispatch, GuppyDownloadRequestParams, Accessibility, downloadFromGuppy } from '@gen3/core'; +import { useState } from 'react'; +import download from '../../utils/download'; + +interface DownloadToFileButtonProps extends GuppyDownloadRequestParams { + filename: string; + format: string; + counts: number; +} + +export const DownloadToFileButton = ( downloadParams : DownloadToFileButtonProps) => { + + const [downloading, setDownloading] = useState(false); + const coreDispatch = useCoreDispatch(); + + const handleSampleSheetDownload = () => { + setDownloading(true); + download({ + endpoint: `${GEN3_GUPPY_API}/download`, + method: "POST", + dispatch: coreDispatch, + params: { + ...downloadParams, + ...(downloadParams.accessibility ? {accessibility: Accessibility.ALL} : {} ) + }, + done: () => setDownloading(false), + }); + }; + + + return ( + handleSampleSheetDownload()} + /> + ); + +}; From 9efc01d6118f8d91ec66254568e99d312c9e42be Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Thu, 8 Feb 2024 16:20:14 -0600 Subject: [PATCH 04/21] Explorer downloads initially working --- package-lock.json | 329 ++-- package.json | 4 +- packages/core/jest.config.ts | 10 +- packages/core/package.json | 18 +- packages/core/rollup.config.mjs | 2 + .../tests/dataGUIDSDotOrg.unit.test.ts | 2 +- .../core/src/features/guppy/conversion.ts | 8 +- .../src/features/guppy/guppyDownloadSlice.ts | 21 +- .../core/src/features/guppy/guppySlice.ts | 2 - packages/core/src/features/guppy/index.ts | 22 +- .../guppy/tests/jsonToFormat.unit.test.ts | 1692 +++++++++++++++++ packages/core/src/features/guppy/types.ts | 38 +- packages/core/src/features/guppy/utils.ts | 150 +- packages/core/src/types/index.ts | 8 +- packages/core/tsconfig.json | 2 + packages/frontend/package.json | 7 +- .../DownloadButtons/DownloadButton.tsx | 16 +- .../CohortBuilder/DownloadButtons.tsx | 155 ++ .../src/features/CohortBuilder/Downloads.tsx | 46 - .../features/CohortBuilder/DownloadsPanel.tsx | 7 +- .../features/CohortBuilder/buttonActions.ts | 1 - .../features/CohortBuilder/buttonActions.tsx | 42 + packages/frontend/src/utils/download.tsx | 37 +- 23 files changed, 2293 insertions(+), 326 deletions(-) create mode 100644 packages/core/src/features/guppy/tests/jsonToFormat.unit.test.ts create mode 100644 packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx delete mode 100644 packages/frontend/src/features/CohortBuilder/Downloads.tsx delete mode 100644 packages/frontend/src/features/CohortBuilder/buttonActions.ts create mode 100644 packages/frontend/src/features/CohortBuilder/buttonActions.tsx diff --git a/package-lock.json b/package-lock.json index 7de5bb71..f5a10828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@swc/core": "^1.3.62", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.1.2", - "@types/jest": "^29.4.0", + "@types/jest": "^29.5.12", "@types/node": "20.4.1", "@types/react": "^18.2.21", "@typescript-eslint/eslint-plugin": "^5.31.0", @@ -45,7 +45,7 @@ "rollup-plugin-swc3": "^0.10.2", "rollup-swc-preserve-directives": "^0.5.0", "terser-webpack-plugin": "^5.3.3", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", "typescript": "5.0.2" } @@ -2802,16 +2802,16 @@ "resolved": "packages/core", "link": true }, - "node_modules/@gen3/crosswalk": { - "resolved": "packages/crosswalk", + "node_modules/@gen3/frontend": { + "resolved": "packages/frontend", "link": true }, - "node_modules/@gen3/datacommonsapp": { + "node_modules/@gen3/sampleCommons": { "resolved": "packages/sampleCommons", "link": true }, - "node_modules/@gen3/frontend": { - "resolved": "packages/frontend", + "node_modules/@gen3/tools": { + "resolved": "packages/tools", "link": true }, "node_modules/@gen3/toolsff": { @@ -8028,6 +8028,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/flat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.5.tgz", + "integrity": "sha512-nPLljZQKSnac53KDUDzuzdRfGI0TDb5qPrb+SrQyN3MtdQrOnGsKniHN1iYZsJEBIVQve94Y6gNz22sgISZq+Q==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "dev": true, @@ -8088,9 +8094,10 @@ } }, "node_modules/@types/jest": { - "version": "29.5.5", + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, - "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -8115,6 +8122,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonpath-plus": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonpath-plus/-/jsonpath-plus-5.0.5.tgz", + "integrity": "sha512-aaqqDf5LcGOtAfEntO5qKZS6ixT0MpNhUXNwbVPdLf7ET9hKsufJq+buZr7eXSnWoLRyGzVj2Yz2hbjVSK3wsA==", + "dev": true + }, "node_modules/@types/keyv": { "version": "3.1.4", "dev": true, @@ -19500,10 +19513,11 @@ "license": "MIT" }, "node_modules/jsonpath-plus": { - "version": "7.2.0", - "license": "MIT", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-8.0.0.tgz", + "integrity": "sha512-+AOBHcQvRr8DcWVIkfOCCCLSlYgQuNZ+gFNqwkBrNpdUfdfkcrbO4ml3F587fWUMFOmoy6D9c+5wrghgjN3mbg==", "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/JSONStream": { @@ -30092,9 +30106,10 @@ "license": "Apache-2.0" }, "node_modules/ts-jest": { - "version": "29.1.1", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", "dev": true, - "license": "MIT", "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", @@ -30109,7 +30124,7 @@ "ts-jest": "cli.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", @@ -32232,7 +32247,10 @@ "@swc/wasm": "^1.3.85", "@testing-library/react": "^14.0.0", "@types/estree": "^1.0.0", + "@types/flat": "^5.0.3", "@types/isomorphic-fetch": "^0.0.36", + "@types/jest": "^29.5.12", + "@types/jsonpath-plus": "5.0.5", "@types/lodash": "^4.14.191", "@types/papaparse": "^5.3.14", "@types/uuid": "^9.0.0", @@ -32242,12 +32260,13 @@ "rollup": "^3.29.2", "rollup-plugin-dts": "^6.0.2", "rollup-plugin-peer-deps-external": "^2.2.4", - "ts-jest": "^29.0.3" + "ts-jest": "^29.1.2" }, "peerDependencies": { "@mantine/core": "^6.0.4", "@mantine/hooks": "^6.0.4", "@reduxjs/toolkit": "1.9.5", + "jsonpath-plus": "^8.0.0", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -32270,6 +32289,7 @@ "packages/crosswalk": { "name": "@gen3/crosswalk", "version": "0.0.1", + "extraneous": true, "license": "Apache-2.0", "dependencies": { "@gen3/core": "0.1.0" @@ -32292,27 +32312,6 @@ "typescript": "5.0.2" } }, - "packages/crosswalk/node_modules/rollup-plugin-dts": { - "version": "5.3.1", - "dev": true, - "license": "LGPL-3.0", - "dependencies": { - "magic-string": "^0.30.2" - }, - "engines": { - "node": ">=v14.21.3" - }, - "funding": { - "url": "https://github.com/sponsors/Swatinem" - }, - "optionalDependencies": { - "@babel/code-frame": "^7.22.5" - }, - "peerDependencies": { - "rollup": "^3.0", - "typescript": "^4.1 || ^5.0" - } - }, "packages/frontend": { "name": "@gen3/frontend", "version": "0.20.2", @@ -32350,7 +32349,7 @@ "gray-matter": "^4.0.3", "jose": "^4.13.1", "js-cookie": "^3.0.5", - "jsonpath-plus": "^7.2.0", + "jsonpath-plus": "^8.0.0", "mantine-react-table": "^1.3.1", "minisearch": "^6.1.0", "next-compose-plugins": "^2.2.1", @@ -32390,6 +32389,7 @@ "@types/file-saver": "^2.0.5", "@types/isomorphic-fetch": "^0.0.36", "@types/jest": "^29.2.3", + "@types/jsonpath-plus": "^5.0.5", "@types/lodash": "^4.14.191", "@types/mdx": "^2.0.3", "@types/node": "18.15.5", @@ -32516,7 +32516,7 @@ } }, "packages/sampleCommons": { - "name": "@gen3/datacommonsapp", + "name": "@gen3/sampleCommons", "version": "0.1.0", "dependencies": { "@gen3/core": "file:../core", @@ -32628,6 +32628,134 @@ "node": ">= 10" } }, + "packages/sampleCommons/node_modules/@next/swc-darwin-x64": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz", + "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz", + "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-linux-arm64-musl": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz", + "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-linux-x64-gnu": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz", + "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-linux-x64-musl": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz", + "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz", + "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz", + "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "packages/sampleCommons/node_modules/@next/swc-win32-x64-msvc": { + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz", + "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "packages/sampleCommons/node_modules/@swc/helpers": { "version": "0.5.1", "dev": true, @@ -33714,8 +33842,9 @@ } }, "packages/tools": { - "name": "@gen3/toolsff", + "name": "@gen3/tools", "version": "0.1.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "@iconify/tools": "^2.1.2", @@ -35430,126 +35559,6 @@ "engines": { "node": ">=10" } - }, - "packages/sampleCommons/node_modules/@next/swc-darwin-x64": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz", - "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "packages/sampleCommons/node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz", - "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "packages/sampleCommons/node_modules/@next/swc-linux-arm64-musl": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz", - "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "packages/sampleCommons/node_modules/@next/swc-linux-x64-gnu": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz", - "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "packages/sampleCommons/node_modules/@next/swc-linux-x64-musl": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz", - "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "packages/sampleCommons/node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz", - "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "packages/sampleCommons/node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz", - "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "packages/sampleCommons/node_modules/@next/swc-win32-x64-msvc": { - "version": "13.4.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz", - "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index d084c21a..d5398dd7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@swc/core": "^1.3.62", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.1.2", - "@types/jest": "^29.4.0", + "@types/jest": "^29.5.12", "@types/node": "20.4.1", "@types/react": "^18.2.21", "@typescript-eslint/eslint-plugin": "^5.31.0", @@ -56,7 +56,7 @@ "rollup-plugin-swc3": "^0.10.2", "rollup-swc-preserve-directives": "^0.5.0", "terser-webpack-plugin": "^5.3.3", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", "typescript": "5.0.2" } diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts index 325023ec..857b668c 100644 --- a/packages/core/jest.config.ts +++ b/packages/core/jest.config.ts @@ -1,6 +1,12 @@ module.exports = { preset: 'ts-jest', - testEnvironment: 'jsdom', + testEnvironment: "node", + transform: { + 'node_modules/(flat)/.+\\.(j|t)s?$': "ts-jest", + }, + transformIgnorePatterns: [ + "node_modules/(?!flat)/" + ], globalSetup: '/setupTests.ts', moduleNameMapper: { '^@/core/(.*)$': '/src/$1', @@ -8,5 +14,5 @@ module.exports = { modulePaths: [''], globals: { fetch: global.fetch, - } + }, }; diff --git a/packages/core/package.json b/packages/core/package.json index 709ae6ad..1105ea2a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -9,15 +9,16 @@ "author": "CTDS", "description": "Core module for gen3 frontend. Provides an interface for interacting with the gen3 API and a redux store for managing state.", "license": "Apache-2.0", - "main": "dist/index.js", + "main": "dist/index.esm.js", "module": "dist/index.esm.js", "unpkg": "dist/index.umd.js", "types": "dist/index.d.ts", "scripts": { "compile": "tsc", "clean": "rimraf dist", + "types": "tsc --emitDeclarationOnly", "build": "rollup --config rollup.config.mjs", - "build:clean": "npm run clean && npm run compile && rollup --config rollup.config.mjs", + "build:clean": "npm run clean && npm run compile && npm run types && rollup --config rollup.config.mjs", "build:watch": "npm run compile && npm run build -- --watch", "test": "jest unit", "test:watch": "jest unit --watch", @@ -30,6 +31,7 @@ "@mantine/hooks": "^6.0.4", "@reduxjs/toolkit": "1.9.5", "lodash": "^4.17.21", + "jsonpath-plus": "^8.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.1.0", @@ -43,9 +45,12 @@ "@swc/wasm": "^1.3.85", "@testing-library/react": "^14.0.0", "@types/estree": "^1.0.0", + "@types/jsonpath-plus": "5.0.5", "@types/isomorphic-fetch": "^0.0.36", + "@types/jest": "^29.5.12", "@types/lodash": "^4.14.191", "@types/papaparse": "^5.3.14", + "@types/flat": "^5.0.3", "@types/uuid": "^9.0.0", "eslint": "^8.36.0", "jest": "^29.7.0", @@ -53,17 +58,16 @@ "rollup": "^3.29.2", "rollup-plugin-dts": "^6.0.2", "rollup-plugin-peer-deps-external": "^2.2.4", - "ts-jest": "^29.0.3" + "ts-jest": "^29.1.2" }, "dependencies": { "flat": "^6.0.1", "papaparse": "^5.4.1" }, - "files": [ - "dist" - ], + "files": [ + "dist" + ], "publishConfig": { "registry": "https://npm.pkg.github.com" } - } diff --git a/packages/core/rollup.config.mjs b/packages/core/rollup.config.mjs index d8279812..23b3103c 100644 --- a/packages/core/rollup.config.mjs +++ b/packages/core/rollup.config.mjs @@ -19,6 +19,8 @@ const globals = { 'react-cookie': 'reactCookie', 'swr': 'swr', 'jsonpath-plus': 'jsonpathPlus', + 'flat': 'flat', + 'papaparse': 'papaparse', }; const config = [ diff --git a/packages/core/src/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.ts b/packages/core/src/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.ts index 4acfe5c6..71ea5641 100644 --- a/packages/core/src/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.ts +++ b/packages/core/src/features/drsResolver/resolvers/tests/dataGUIDSDotOrg.unit.test.ts @@ -29,7 +29,7 @@ describe('resolveDRSWithDataGUISOrg', () => { it('should return empty object if input is an empty array', async () => { const guidsForHostnameResolution: string[] = []; - await expect(resolveDRSWithDataGUISOrg(guidsForHostnameResolution)).toEqual( + expect(resolveDRSWithDataGUISOrg(guidsForHostnameResolution)).toEqual( expect.objectContaining({}), ); }); diff --git a/packages/core/src/features/guppy/conversion.ts b/packages/core/src/features/guppy/conversion.ts index fa14cf34..777789e0 100644 --- a/packages/core/src/features/guppy/conversion.ts +++ b/packages/core/src/features/guppy/conversion.ts @@ -1,4 +1,4 @@ -import { JSONArray, JSONObject } from "../../types"; +import { JSONArray, JSONObject } from '../../types'; import { FILE_DELIMITERS } from '../../constants'; import { flatten } from 'flat'; import Papa, { UnparseConfig } from 'papaparse'; @@ -10,7 +10,7 @@ import Papa, { UnparseConfig } from 'papaparse'; * @param {JSON} json */ export function flattenJson(json: JSONObject) { - const flattenedJson : JSONArray = []; + const flattenedJson: JSONArray = []; Object.keys(json).forEach((key) => { flattenedJson.push(flatten(json[key], { delimiter: '_' })); }); @@ -28,7 +28,7 @@ export async function conversion(json: JSONArray, config: UnparseConfig) { /** * Converts JSON to a specified file format. - * Defaultes to JSON if file format is not supported. + * Defaults to JSON if file format is not supported. * @param {JSON} json * @param {string} format */ @@ -40,7 +40,7 @@ export async function jsonToFormat( const flatJson = await flattenJson(json); const data = await conversion(flatJson, { delimiter: FILE_DELIMITERS[format] as string, - } ); + }); return data; } return json; diff --git a/packages/core/src/features/guppy/guppyDownloadSlice.ts b/packages/core/src/features/guppy/guppyDownloadSlice.ts index 5add29c2..b7375007 100644 --- a/packages/core/src/features/guppy/guppyDownloadSlice.ts +++ b/packages/core/src/features/guppy/guppyDownloadSlice.ts @@ -1,7 +1,11 @@ import { gen3Api } from '../gen3'; import { GEN3_GUPPY_API } from '../../constants'; -import { convertFilterSetToGqlFilter } from '../filters'; -import { GuppyDownloadQueryParams, GuppyDownloadRequestParams } from './types'; +import { convertFilterSetToGqlFilter, GQLFilter } from '../filters'; +import { BaseGuppyDataRequest, GuppyDownloadDataParams } from './types'; + +export interface GuppyDownloadDataQueryParams extends BaseGuppyDataRequest { + filter: GQLFilter; +} interface DownloadRequestStatus { readonly status: string; @@ -10,22 +14,23 @@ interface DownloadRequestStatus { export const downloadRequestApi = gen3Api.injectEndpoints({ endpoints: (builder) => ({ - downloadFromGuppy: builder.query({ + downloadFromGuppy: builder.mutation({ query: ({ type, - filters, + filter, accessibility, fields, sort, - }: GuppyDownloadQueryParams) => { - const queryBody: GuppyDownloadRequestParams = { - filter: convertFilterSetToGqlFilter(filters), + }: GuppyDownloadDataParams) => { + const queryBody: GuppyDownloadDataQueryParams = { + filter: convertFilterSetToGqlFilter(filter), ...{ type, accessibility, fields, sort }, }; return { url: `${GEN3_GUPPY_API}/download`, method: 'POST', queryBody, + cache: 'no-cache', }; }, transformResponse: (response: DownloadRequestStatus) => { @@ -35,4 +40,4 @@ export const downloadRequestApi = gen3Api.injectEndpoints({ }), }); -export const { useDownloadFromGuppyQuery } = downloadRequestApi; +export const { useDownloadFromGuppyMutation } = downloadRequestApi; diff --git a/packages/core/src/features/guppy/guppySlice.ts b/packages/core/src/features/guppy/guppySlice.ts index db275716..0002db69 100644 --- a/packages/core/src/features/guppy/guppySlice.ts +++ b/packages/core/src/features/guppy/guppySlice.ts @@ -101,8 +101,6 @@ interface QueryCountsParams { accessibility?: Accessibility; } - - const explorerApi = guppyApi.injectEndpoints({ endpoints: (builder) => ({ getAllFieldsForType: builder.query({ diff --git a/packages/core/src/features/guppy/index.ts b/packages/core/src/features/guppy/index.ts index 9a040ee9..54bc7ede 100644 --- a/packages/core/src/features/guppy/index.ts +++ b/packages/core/src/features/guppy/index.ts @@ -1,7 +1,21 @@ export * from './guppylApi'; export * from './guppySlice'; -export * from './guppyDownloadSlice'; -export * from './utils'; -import { type GuppyDownloadRequestParams, type GuppyDownloadQueryParams } from './types'; +import { downloadFromGuppy } from './utils'; +import { useDownloadFromGuppyMutation } from './guppyDownloadSlice'; +import { + type GuppyDownloadDataParams, + type BaseGuppyDataRequest, + type GuppyActionFunction, + type GuppyActionFunctionParams, + type GuppyDownloadActionFunctionParams, +} from './types'; -export { type GuppyDownloadRequestParams, type GuppyDownloadQueryParams }; +export { + type BaseGuppyDataRequest, + type GuppyDownloadDataParams, + type GuppyActionFunctionParams, + type GuppyActionFunction, + type GuppyDownloadActionFunctionParams, + downloadFromGuppy, + useDownloadFromGuppyMutation, +}; diff --git a/packages/core/src/features/guppy/tests/jsonToFormat.unit.test.ts b/packages/core/src/features/guppy/tests/jsonToFormat.unit.test.ts new file mode 100644 index 00000000..ee1c1932 --- /dev/null +++ b/packages/core/src/features/guppy/tests/jsonToFormat.unit.test.ts @@ -0,0 +1,1692 @@ +import { jsonToFormat } from '../conversion'; +import { JSONObject } from '../../../types'; +import * as flat from 'flat'; +import { FILE_DELIMITERS } from '../../../constants'; + +describe('jsonToFormat function', () => { + afterEach(() => jest.clearAllMocks()); + + it('should return JSON in specified file format', async () => { + const jsonInput: JSONObject = { key1: 'value1', key2: 'value2' }; + + const jsonInput2 = { + data: { + case: [ + { + submitter_id: '639127-000542', + sex: 'Not Reported', + age_at_index: 79, + index_event: 'First COVID test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: '482', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 80, + body_part_examined: ['CHEST'], + days_to_study: -52, + loinc_code: '43468-8', + loinc_contrast: null, + loinc_long_common_name: 'XR Unspecified body region Views', + loinc_method: 'XR', + loinc_system: 'Unspecified', + study_description: null, + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.671385.4752', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 0, + _cr_series_file_count: 1, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-009181', + sex: 'Not Reported', + age_at_index: 29, + index_event: 'COVID-19 Test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: '946', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 29, + body_part_examined: ['CHEST'], + days_to_study: 322, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.241777105162290781131991169908', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: 366, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 336, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 320, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 147, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 293, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 499, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 324, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 287, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 403, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: [ + { + days_to_medication_start: 213, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 711, + days_to_medication_end: null, + dose_sequence_number: 4, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 453, + days_to_medication_end: null, + dose_sequence_number: 3, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 234, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 860, + days_to_medication_end: null, + dose_sequence_number: 5, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.252551449607234369736877729753', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '514382-012965', + sex: 'Not Reported', + age_at_index: 36, + index_event: 'COVID-19 Test', + race: 'Other', + ethnicity: 'Hispanic or Latino', + zip: 'US', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 36, + body_part_examined: ['CHEST'], + days_to_study: 45, + loinc_code: '30745-4', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest Views', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'Chest', + study_location: null, + study_modality: ['DX'], + study_uid: '1.2.826.0.1.3680043.10.474.514382.1567317', + }, + { + age_at_imaging: 36, + body_part_examined: ['PORT CHEST'], + days_to_study: 1, + loinc_code: '36554-4', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest Single view', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VW, FRONTAL', + study_location: null, + study_modality: ['DX'], + study_uid: '1.2.826.0.1.3680043.10.474.514382.1381291', + }, + { + age_at_imaging: 36, + body_part_examined: ['PORT CHEST'], + days_to_study: -2, + loinc_code: '36554-4', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest Single view', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VW, FRONTAL', + study_location: null, + study_modality: ['DX'], + study_uid: '1.2.826.0.1.3680043.10.474.514382.1712555', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 453, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: null, + test_days_from_index: 407, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 3, + _ct_series_file_count: 0, + _dx_series_file_count: 5, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-012398', + sex: 'Not Reported', + age_at_index: 79, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 79, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.310464004523196691764117439716', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: [ + { + days_to_medication_start: 8, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.459182072283698234299289198341', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-007513', + sex: 'Not Reported', + age_at_index: 60, + index_event: 'COVID-19 Test', + race: 'Not Reported', + ethnicity: 'Hispanic or Latino', + zip: '941', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 60, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.474092284662271536629184790059', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 11, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 14, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 25, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: 5, + annotation_method: null, + annotator_id: null, + instance_uids: null, + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.810741460028941099018374381217', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '232451-001091', + sex: 'Not Reported', + age_at_index: 13, + index_event: 'First COVID test', + race: 'Black or African American', + ethnicity: 'Not Hispanic or Latino', + zip: 'US', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 13, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR P PORT CHEST 1 VIEW', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.55472', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 0, + _cr_series_file_count: 1, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '514382-039181', + sex: 'Not Reported', + age_at_index: 21, + index_event: 'COVID-19 Test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: 'US', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 21, + body_part_examined: ['PORT CHEST'], + days_to_study: 0, + loinc_code: '36554-4', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest Single view', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST PICC VERIFICATION', + study_location: null, + study_modality: ['DX'], + study_uid: '1.2.826.0.1.3680043.10.474.514382.6029171', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 2, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 2, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-010559', + sex: 'Not Reported', + age_at_index: 56, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 56, + body_part_examined: ['CHEST'], + days_to_study: -2, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.180563485118550787879260910806', + }, + { + age_at_imaging: 56, + body_part_examined: ['CHEST'], + days_to_study: 5, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.367230495062884622986946202074', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -83, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -88, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -6, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -10, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -50, + }, + ], + medications: [ + { + days_to_medication_start: 214, + days_to_medication_end: null, + dose_sequence_number: 5, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 123, + days_to_medication_end: null, + dose_sequence_number: 4, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: -137, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 242, + days_to_medication_end: null, + dose_sequence_number: 6, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: -116, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 269, + days_to_medication_end: null, + dose_sequence_number: 7, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 102, + days_to_medication_end: null, + dose_sequence_number: 3, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.163843159339111198747559950040', + ], + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.268158452269688349584707601394', + ], + }, + ], + _imaging_studies_count: 2, + _ct_series_file_count: 0, + _dx_series_file_count: 3, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-006028', + sex: 'Not Reported', + age_at_index: 38, + index_event: 'COVID-19 Test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: '947', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 38, + body_part_examined: ['CHEST'], + days_to_study: 239, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.724627111204085272645675138832', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 562, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 528, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 239, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 195, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 450, + }, + ], + medications: [ + { + days_to_medication_start: 621, + days_to_medication_end: null, + dose_sequence_number: 3, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 384, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 412, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.250985117089692904162560720014', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '232451-001655', + sex: 'Not Reported', + age_at_index: 63, + index_event: 'First COVID test', + race: 'Black or African American', + ethnicity: 'Not Hispanic or Latino', + zip: 'US', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 3, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63901', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63904', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63913', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 4, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63898', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 1, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63907', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 2, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63895', + }, + { + age_at_imaging: 63, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '36589-0', + loinc_contrast: null, + loinc_long_common_name: 'Portable XR Chest AP single view', + loinc_method: 'XR.portable', + loinc_system: 'Chest', + study_description: 'XR PORT CHEST 1V', + study_location: null, + study_modality: ['CR'], + study_uid: '1.2.826.0.1.3680043.10.474.232451.63910', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 2, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: null, + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 7, + _ct_series_file_count: 0, + _dx_series_file_count: 0, + _cr_series_file_count: 7, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-009373', + sex: 'Not Reported', + age_at_index: 37, + index_event: 'COVID-19 Test', + race: 'White', + ethnicity: 'Not Hispanic or Latino', + zip: '941', + covid19_positive: 'No', + imaging_studies: [ + { + age_at_imaging: 37, + body_part_examined: ['CHEST'], + days_to_study: 212, + loinc_code: '42272-5', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest PA and Lateral', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 2 VIEWS PA AND LATERAL', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.507061461553432315196526618864', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -126, + }, + ], + medications: [ + { + days_to_medication_start: 58, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 82, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.846240764585028846016503187378', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-011913', + sex: 'Not Reported', + age_at_index: 71, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 71, + body_part_examined: ['CHEST'], + days_to_study: 9, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.278107455876066137171677606988', + }, + { + age_at_imaging: 71, + body_part_examined: ['CHEST'], + days_to_study: 3, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.328243271539180794824930002963', + }, + { + age_at_imaging: 71, + body_part_examined: null, + days_to_study: 12, + loinc_code: '29252-4', + loinc_contrast: 'WO', + loinc_long_common_name: 'CT Chest WO contrast', + loinc_method: 'CT', + loinc_system: 'Chest', + study_description: 'CT CHEST WITHOUT CONTRAST', + study_location: null, + study_modality: ['CT'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.105731777963798038123529567873', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -388, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 7, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -584, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -355, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -227, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -289, + }, + ], + medications: [ + { + days_to_medication_start: -250, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: -271, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.519200256772382868331768076862', + ], + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: 8, + annotation_method: null, + annotator_id: null, + instance_uids: null, + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: 1, + annotation_method: null, + annotator_id: null, + instance_uids: null, + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.126190299596310978922819788660', + ], + }, + ], + _imaging_studies_count: 3, + _ct_series_file_count: 7, + _dx_series_file_count: 2, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-012375', + sex: 'Not Reported', + age_at_index: 60, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 60, + body_part_examined: ['CHEST'], + days_to_study: 0, + loinc_code: '42272-5', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest PA and Lateral', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 2 VIEWS PA AND LATERAL', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.330957237716414669587476460080', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'Rapid antigen test', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -97, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: -98, + }, + ], + medications: [ + { + days_to_medication_start: 26, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91303', + medication_code_system: 'CPT', + medication_manufacturer: 'Janssen', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: -37, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91303', + medication_code_system: 'CPT', + medication_manufacturer: 'Janssen', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.180137370330907200957672927457', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-007947', + sex: 'Not Reported', + age_at_index: 89, + index_event: 'COVID-19 Test', + race: 'Asian', + ethnicity: 'Not Hispanic or Latino', + zip: '941', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 89, + body_part_examined: ['CHEST'], + days_to_study: 61, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.222552150766148875375871550478', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: 61, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 239, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 242, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -118, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -3, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -190, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'Rapid antigen test', + test_days_from_index: -118, + }, + ], + medications: [ + { + days_to_medication_start: 341, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91301', + medication_code_system: 'CPT', + medication_manufacturer: 'Moderna', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: null, + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 0, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-012433', + sex: 'Not Reported', + age_at_index: 70, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 70, + body_part_examined: ['CHEST'], + days_to_study: 8, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.319316423651295112004324887047', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -285, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 8, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -243, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -93, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -224, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -294, + }, + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: -165, + }, + ], + medications: [ + { + days_to_medication_start: -289, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91303', + medication_code_system: 'CPT', + medication_manufacturer: 'Janssen', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: 2, + annotation_method: null, + annotator_id: null, + instance_uids: null, + }, + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.164033539388468242078934854691', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + { + submitter_id: '419639-010716', + sex: 'Not Reported', + age_at_index: 56, + index_event: null, + race: 'Not Reported', + ethnicity: 'Not Hispanic or Latino', + zip: '0', + covid19_positive: 'Yes', + imaging_studies: [ + { + age_at_imaging: 56, + body_part_examined: ['CHEST'], + days_to_study: 31, + loinc_code: '36572-6', + loinc_contrast: null, + loinc_long_common_name: 'XR Chest AP', + loinc_method: 'XR', + loinc_system: 'Chest', + study_description: 'XR CHEST 1 VIEW AP', + study_location: null, + study_modality: ['DX'], + study_uid: + '1.2.826.0.1.3680043.10.474.419639.127832308794752991138261441788', + }, + ], + measurements: [ + { + test_name: 'COVID-19', + test_result_text: 'Negative', + test_method: 'RT-PCR', + test_days_from_index: 28, + }, + { + test_name: 'COVID-19', + test_result_text: 'Positive', + test_method: 'RT-PCR', + test_days_from_index: 0, + }, + ], + medications: [ + { + days_to_medication_start: 217, + days_to_medication_end: null, + dose_sequence_number: 3, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 53, + days_to_medication_end: null, + dose_sequence_number: 2, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 457, + days_to_medication_end: null, + dose_sequence_number: 4, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + { + days_to_medication_start: 32, + days_to_medication_end: null, + dose_sequence_number: 1, + medication_code: '91300', + medication_code_system: 'CPT', + medication_manufacturer: 'Pfizer', + medication_name: 'COVID-19 Vaccine', + medication_status: null, + medication_type: 'Vaccine', + }, + ], + procedures: null, + conditions: null, + imaging_study_annotations: [ + { + airspace_disease_grading: null, + class_covid19_pneumonia: null, + midrc_mRALE_score: null, + annotation_method: 'Retrospective_auto', + annotator_id: 'SIFT', + instance_uids: [ + '1.2.826.0.1.3680043.10.474.419639.340258858670915453041960131347', + ], + }, + ], + _imaging_studies_count: 1, + _ct_series_file_count: 0, + _dx_series_file_count: 1, + _cr_series_file_count: 0, + _mr_series_file_count: 0, + project_id: 'Open-R1', + }, + ], + _aggregation: { + case: { + _totalCount: 16, + }, + }, + }, + }; + + const format: keyof typeof FILE_DELIMITERS = 'csv'; + + const spyFlat = jest.spyOn(flat, 'flatten').mockReturnValueOnce(jsonInput2); + + const result = await jsonToFormat(jsonInput, format); + + expect(result).toBeInstanceOf(Object); + expect(result).toBe(jsonInput); + expect(spyFlat).toHaveBeenCalledWith(jsonInput); + }); +}); diff --git a/packages/core/src/features/guppy/types.ts b/packages/core/src/features/guppy/types.ts index 8491b7f8..932f805c 100644 --- a/packages/core/src/features/guppy/types.ts +++ b/packages/core/src/features/guppy/types.ts @@ -1,18 +1,40 @@ -import { FilterSet, GQLFilter } from '../filters'; +import { FilterSet } from '../filters'; import { Accessibility } from '../../constants'; -export interface BaseGuppyDownloadRequest { +export interface BaseGuppyDataRequest { type: string; accessibility?: Accessibility; - fields?: string[]; + fields: string[]; sort?: string[]; - format?: string; } -export interface GuppyDownloadQueryParams extends BaseGuppyDownloadRequest { - filters: FilterSet; +export interface GuppyDownloadDataParams extends BaseGuppyDataRequest { + filter: FilterSet; // cohort filters + format: 'json' | 'csv' | 'tsv'; // the three supported formats + rootPath?: string; // a string (minus $.) JSONPath to the root of the data } -export interface GuppyDownloadRequestParams extends BaseGuppyDownloadRequest { - readonly filter: GQLFilter; +export interface GuppyActionFunctionParams extends Record { + type: string; + accessibility?: Accessibility; + fields: string[]; + sort?: string[]; + filter: FilterSet; +} + +export interface GuppyActionParams> { + parameters: T; + onStart?: () => void; + onDone?: (blob: Blob) => void; + onError?: (error: Error) => void; +} + +export interface GuppyDownloadActionFunctionParams + extends GuppyActionFunctionParams { + format: string; + filename: string; } + +export type GuppyActionFunction> = ( + params: GuppyActionParams, +) => void; diff --git a/packages/core/src/features/guppy/utils.ts b/packages/core/src/features/guppy/utils.ts index 4ab59278..a2f764dd 100644 --- a/packages/core/src/features/guppy/utils.ts +++ b/packages/core/src/features/guppy/utils.ts @@ -1,65 +1,113 @@ - -import { GuppyDownloadQueryParams } from './types'; +import { GuppyActionParams, GuppyDownloadDataParams } from './types'; import { GEN3_GUPPY_API } from '../../constants'; +import { selectCSRFToken } from '../gen3'; +import { coreStore } from '../../store'; +import { convertFilterSetToGqlFilter } from '../filters'; +import { jsonToFormat } from './conversion'; +import { isJSONObject } from '../../types'; +import { JSONPath } from 'jsonpath-plus'; -interface GuppyFileDownloadRequestParams extends GuppyDownloadQueryParams { - readonly format: string; - readonly filename: string; - readonly total: number; -} +export type DownloadFromGuppyOptions = + GuppyActionParams; -type DownloadOptions = { - method: 'GET' | 'POST'; - parameters?: GuppyFileDownloadRequestParams; - requestHeaders?: Record; - onStart?: () => void; - onEnd?: () => void; - onError?: (error: Error) => void; +/** + * Represents a configuration for making a fetch request. + * + * @typedef {Object} FetchConfig + * @property {string} method - The HTTP method to use for the request. + * @property {Object} headers - The headers to include in the request. + * @property {string} body - The request body. + */ +export type FetchConfig = { + method: string; + headers: Record; + body: string; }; -export const downloadFromGuppy = ({ - parameters, - method = 'POST', - requestHeaders = {}, - onStart = () => null, - onEnd = () => null, - onError = () => null, - }: DownloadOptions) => { - if (onStart) { - onStart(); - } - const url = new URL(GEN3_GUPPY_API + '/download'); - if (parameters) { - url.search = new URLSearchParams(parameters as any).toString(); - } - fetch(url.toString(), { - method, +/** + * Prepares a URL for downloading by appending '/download' to the provided apiUrl. + * + * @param {string} apiUrl - The base URL to be used for preparing the download URL. + * @returns {URL} - The prepared download URL as a URL object. + */ +const prepareUrl = (apiUrl: string) => new URL(apiUrl + '/download'); + +/** + * Prepares a fetch configuration object for downloading files from Guppy. + * + * @param {GuppyFileDownloadRequestParams} parameters - The parameters to include in the request body. + * @param {string} csrfToken - The CSRF token to include in the request headers. + * @returns {FetchConfig} - The prepared fetch configuration object. + */ +const prepareFetchConfig = ( + parameters: GuppyDownloadDataParams, + csrfToken?: string, +): FetchConfig => { + return { + method: 'POST', headers: { 'Content-Type': 'application/json', - ...requestHeaders, + ...(csrfToken !== undefined && { 'X-CSRFToken': csrfToken }), }, - }) - .then((response) => { + body: JSON.stringify({ + type: parameters.type, + filter: convertFilterSetToGqlFilter(parameters.filter), + accessibility: parameters.accessibility, + fields: parameters?.fields, + sort: parameters?.sort, + }), + }; +}; + +/** + * Downloads a file from Guppy using the provided parameters. + * It will optionally convert the data to the specified format. + * + * @param {DownloadFromGuppyOptions} parameters - The parameters to use for the download request. + * @param onStart - The function to call when the download starts. + * @param onDone - The function to call when the download is done. + * @param onError - The function to call when the download fails. + */ +export const downloadFromGuppy = ({ + parameters, + onStart = () => null, + onDone = (_: Blob) => null, + onError = (_: Error) => null, +}: DownloadFromGuppyOptions) => { + const csrfToken = selectCSRFToken(coreStore.getState()); + onStart?.(); + + const url = prepareUrl(GEN3_GUPPY_API); + const fetchConfig = prepareFetchConfig(parameters, csrfToken); + + fetch(url.toString(), fetchConfig) + .then(async (response: Response) => { if (!response.ok) { throw new Error(response.statusText); } - return response.blob(); - }) - .then((blob) => { - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = parameters?.filename || 'download'; - document.body.appendChild(a); - a.click(); - a.remove(); - if (onEnd) { - onEnd(); + let jsonData = await response.json(); + if (parameters?.rootPath && parameters.rootPath) { // if rootPath is provided, extract the data from the rootPath + jsonData = JSONPath({ + json: jsonData, + path: `$.[${parameters.rootPath}]`, + resultType: 'value', + }); } - }) - .catch((error) => { - if (onError) { - onError(error); + // convert the data to the specified format and return a Blob + let str = ''; + if (parameters.format === 'json') { + str = JSON.stringify(jsonData); + } else { + const convertedData = await jsonToFormat(jsonData, parameters.format); + if (isJSONObject(convertedData)) { + str = JSON.stringify(convertedData); + } else { + str = convertedData; + } } - }); + const bytes = new TextEncoder().encode(str); + return new Blob([bytes]); + }) + .then((blob) => onDone?.(blob)) + .catch((error) => onError?.(error)); }; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 1ccb53e9..dd920b1b 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -19,12 +19,16 @@ export interface HistogramData { count: number; } -export type HistogramDataArray = Array; - +// type guard functions export const isHistogramRangeData = (key: any): key is [number, number] => { return Array.isArray(key) && key.length === 2 && key.every((item) => typeof item === 'number'); }; +export const isJSONObject = (data: any): data is JSONObject => { + return typeof data === 'object' && data !== null && !Array.isArray(data); +}; + +export type HistogramDataArray = Array; export const isHistogramData = (data: any): data is HistogramData => { return typeof data === 'object' && data !== null && 'key' in data && 'count' in data; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 67f58b23..c6ce465f 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -13,6 +13,7 @@ "esModuleInterop": true, "declaration": true, "emitDeclarationOnly": true, + "strictNullChecks": true, "module": "esnext", "moduleResolution": "bundler", "noUnusedLocals": true, @@ -23,6 +24,7 @@ "target": "esnext", "resolveJsonModule": true, "sourceMap": true, + "allowJs": true, "lib": [ "dom", "esnext" diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 8f40f82b..b955d426 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -26,9 +26,9 @@ "copy-tailwind": "mkdir -p dist && cp src/tailwind.cjs dist/tailwind.cjs", "copy-style": "mkdir -p dist/styles && cp src/styles/* dist/styles/", "build": "npm run build:rollup && npm run types", - "build:clean": "npm run clean && npm run build", + "build:clean": "npm run clean && npm run build && npm run types", "build:rollup": "npm run compile && npm run copy-tailwind && rollup --config rollup.config.mjs", - "build:compile": "npm run compile && npm run build:rollup", + "build:compile": "npm run compile && npm run types && npm run build:rollup", "build:swc": "npm run compile && npm run copy-tailwind && swc src -d dist", "build:watch": "npm run compile && npm run build:rollup -- --watch", "dev": "npm run build:watch" @@ -77,7 +77,7 @@ "gray-matter": "^4.0.3", "jose": "^4.13.1", "js-cookie": "^3.0.5", - "jsonpath-plus": "^7.2.0", + "jsonpath-plus": "^8.0.0", "mantine-react-table": "^1.3.1", "minisearch": "^6.1.0", "next-compose-plugins": "^2.2.1", @@ -116,6 +116,7 @@ "@types/estree": "^1.0.0", "@types/file-saver": "^2.0.5", "@types/isomorphic-fetch": "^0.0.36", + "@types/jsonpath-plus": "^5.0.5", "@types/jest": "^29.2.3", "@types/lodash": "^4.14.191", "@types/mdx": "^2.0.3", diff --git a/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx b/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx index 01cbdf09..5b4af084 100644 --- a/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx +++ b/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx @@ -1,6 +1,6 @@ import { Button, ButtonProps, Loader, Tooltip } from '@mantine/core'; import { FiDownload } from 'react-icons/fi'; -import download from '../../utils/download'; +import download, { DownloadFunctionParams } from '../../utils/download'; import { hideModal, Modals, useCoreDispatch } from '@gen3/core'; import { Dispatch, SetStateAction, forwardRef } from 'react'; @@ -28,7 +28,7 @@ import { Dispatch, SetStateAction, forwardRef } from 'react'; * @property toolTip - The tooltip to display. */ interface DownloadButtonProps { - endpoint: string; + endpoint?: string; disabled?: boolean; inactiveText: string; activeText: string; @@ -75,13 +75,15 @@ interface DownloadButtonProps { * @param toolTip - The tooltip to display. * @category Buttons */ + + export const DownloadButton = forwardRef< HTMLButtonElement, DownloadButtonProps & ButtonProps >( ( { - endpoint, + endpoint = '', disabled = false, inactiveText, activeText, @@ -94,8 +96,8 @@ export const DownloadButton = forwardRef< showIcon = true, preventClickEvent = false, active, - Modal400, - Modal403, + Modal400 = Modals.GeneralErrorModal, + Modal403 = Modals.NoAccessModal, toolTip, ...buttonProps }: DownloadButtonProps, @@ -130,11 +132,11 @@ export const DownloadButton = forwardRef< dispatch(hideModal()); setActive && setActive(true); download({ - params, endpoint, + params, method, - done: () => setActive && setActive(false), dispatch, + done: () => setActive && setActive(false), Modal400, Modal403, }); diff --git a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx new file mode 100644 index 00000000..9c2ca780 --- /dev/null +++ b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx @@ -0,0 +1,155 @@ +import { + Accessibility, + type GuppyDownloadActionFunctionParams, + hideModal, + Modals, + showModal, + useCoreDispatch, +} from '@gen3/core'; +import { Dispatch, SetStateAction, useRef, useState } from 'react'; +import { downloadToFileAction } from './buttonActions'; +import { Button, Loader, Tooltip } from '@mantine/core'; +import { FiDownload } from 'react-icons/fi'; +import { useDeepCompareCallback } from 'use-deep-compare'; +import { partial } from 'lodash'; + +type ActionButtonFunction = ( + done?: () => void, + onError?: (error: Error) => void) => void; + + +interface GuppyActionButtonProps { + disabled?: boolean; + inactiveText: string; + activeText: string; + customStyle?: string; + showLoading?: boolean; + showIcon?: boolean; + preventClickEvent?: boolean; + onClick?: () => void; + setActive?: Dispatch>; + active?: boolean; + Modal403?: Modals; + Modal400?: Modals; + toolTip?: string; + actionFunction: ActionButtonFunction; + customErrorMessage?: string; +} + + +const GuppyActionButton = ( + { + active, + activeText, + inactiveText, + customStyle, + showLoading = true, + showIcon = true, + preventClickEvent = false, + onClick, + setActive, + disabled = false, + Modal403 = Modals.NoAccessModal, + Modal400 = Modals.GeneralErrorModal, + toolTip, + customErrorMessage, + actionFunction, + }: GuppyActionButtonProps, +) => { + const text = active ? activeText : inactiveText; + const ref = useRef(null); + const dispatch = useCoreDispatch(); + + + const handleError = useDeepCompareCallback((error: Error) => { + const errorMessage: string = error.message; + if ( + errorMessage === 'internal server error' || + errorMessage === undefined + ) { + dispatch(showModal({ modal: Modal400, message: errorMessage })); + } else if ( + errorMessage === + 'Your token is invalid or expired. Please get a new token.' + ) { + dispatch(showModal({ modal: Modal403, message: errorMessage })); + } else { + dispatch( + showModal({ + modal: Modal400, + message: customErrorMessage || errorMessage, + }), + ); + } + }, [Modal400, Modal403, customErrorMessage, dispatch]); + + + const Icon = active ? ( + + ) : ( + + ); + return ( + + + + ); +}; + + +export const DownloadToFileButton = ({ + type, + filename, + filter, + fields, + accessibility, + }: GuppyDownloadActionFunctionParams) => { + const [downloading, setDownloading] = useState(false); + + return ( + + ); + +}; diff --git a/packages/frontend/src/features/CohortBuilder/Downloads.tsx b/packages/frontend/src/features/CohortBuilder/Downloads.tsx deleted file mode 100644 index 96f438f7..00000000 --- a/packages/frontend/src/features/CohortBuilder/Downloads.tsx +++ /dev/null @@ -1,46 +0,0 @@ - - -import { DownloadButton } from '../../components/DownloadButtons/DownloadButton'; -import { GEN3_GUPPY_API, useCoreDispatch, GuppyDownloadRequestParams, Accessibility, downloadFromGuppy } from '@gen3/core'; -import { useState } from 'react'; -import download from '../../utils/download'; - -interface DownloadToFileButtonProps extends GuppyDownloadRequestParams { - filename: string; - format: string; - counts: number; -} - -export const DownloadToFileButton = ( downloadParams : DownloadToFileButtonProps) => { - - const [downloading, setDownloading] = useState(false); - const coreDispatch = useCoreDispatch(); - - const handleSampleSheetDownload = () => { - setDownloading(true); - download({ - endpoint: `${GEN3_GUPPY_API}/download`, - method: "POST", - dispatch: coreDispatch, - params: { - ...downloadParams, - ...(downloadParams.accessibility ? {accessibility: Accessibility.ALL} : {} ) - }, - done: () => setDownloading(false), - }); - }; - - - return ( - handleSampleSheetDownload()} - /> - ); - -}; diff --git a/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx b/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx index ef96375e..95b68056 100644 --- a/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx +++ b/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx @@ -7,7 +7,7 @@ import { } from '../../components/Buttons/DropdownButtons'; import ActionButton from '../../components/Buttons/ActionButton'; import { FilterSet, useIsUserLoggedIn, convertFilterSetToGqlFilter } from '@gen3/core'; -import { DownloadToFileButton } from './Downloads'; +import { DownloadToFileButton } from './DownloadButtons'; @@ -66,9 +66,10 @@ const DownloadsPanel = ({ type={index} counts={totalCount} fields={fields} - filter={convertFilterSetToGqlFilter(filters)} - filename={`cohort_${index}`} + filter={filters} + filename={`${index}.json`} format="json" + /> {Object.values(dropdownsToRender).map((dropdown) => ( diff --git a/packages/frontend/src/features/CohortBuilder/buttonActions.ts b/packages/frontend/src/features/CohortBuilder/buttonActions.ts deleted file mode 100644 index 868b914e..00000000 --- a/packages/frontend/src/features/CohortBuilder/buttonActions.ts +++ /dev/null @@ -1 +0,0 @@ -import { GEN3_GUPPY_API, useCoreDispatch, GuppyDownloadRequestParams, Accessibility } from '@gen3/core'; diff --git a/packages/frontend/src/features/CohortBuilder/buttonActions.tsx b/packages/frontend/src/features/CohortBuilder/buttonActions.tsx new file mode 100644 index 00000000..6284f895 --- /dev/null +++ b/packages/frontend/src/features/CohortBuilder/buttonActions.tsx @@ -0,0 +1,42 @@ +import { + GuppyDownloadDataParams, + downloadFromGuppy, +} from '@gen3/core'; + +interface DownloadFileFromGuppyParams extends GuppyDownloadDataParams { + filename: string; +} + +export interface DownloadActionParams { + done?: () => void; + onError?: (error: Error) => void; +} + +const handleDownload = (data: Blob, filename: string) => { + const aElement = document.createElement('a'); + const href = URL.createObjectURL(data); + aElement.setAttribute('download', filename); + aElement.href = href; + aElement.setAttribute('target', '_blank'); + aElement.click(); + URL.revokeObjectURL(href); +}; + +export const downloadToFileAction = async ( + params: DownloadFileFromGuppyParams, + done?: () => void, + onError?: (error: Error) => void, +): Promise => { + const handleData = (data: Blob) => { + handleDownload(data, params.filename); + done && done(); + }; + + downloadFromGuppy({ + parameters: params, + onDone: (data: Blob) => handleData(data), + onError: (error: Error) => { + onError && onError(error); + }, + }); +}; diff --git a/packages/frontend/src/utils/download.tsx b/packages/frontend/src/utils/download.tsx index 6db5d134..e07f47ed 100644 --- a/packages/frontend/src/utils/download.tsx +++ b/packages/frontend/src/utils/download.tsx @@ -1,4 +1,3 @@ - import Cookies from 'universal-cookie'; import { CoreDispatch, Modals, showModal } from '@gen3/core'; import { Button } from '@mantine/core'; @@ -80,6 +79,24 @@ const SlowDownloadNotification = ({ onClick }: { onClick: () => void }) => ( ); */ +export interface DownloadFunctionParams< + T extends Record = Record, +> { + endpoint: string; + params: T; + method: string; + dispatch: CoreDispatch; + done?: () => void; + Modal403?: Modals; + Modal400?: Modals; + customErrorMessage?: string; + hideNotification?: boolean; +} + +export type DownloadFunction< + T extends Record = Record, +> = (params: DownloadFunctionParams) => Promise; + /** * Trigger a download by attaching an iFrame to the document with the parameters of the request as fields in a form * @param endpoint - endpoint to be attached with @@ -93,7 +110,7 @@ const SlowDownloadNotification = ({ onClick }: { onClick: () => void }) => ( * @param hideNotification - hide the notification */ -const download = async ({ +const download = async = Record>({ endpoint, params, method, @@ -103,17 +120,7 @@ const download = async ({ Modal403 = Modals.NoAccessModal, customErrorMessage, hideNotification = false, -}: { - endpoint: string; - params: Record; - method: string; - dispatch: CoreDispatch; - done?: () => void; - Modal403?: Modals; - Modal400?: Modals; - customErrorMessage?: string; - hideNotification?: boolean; -}): Promise => { +}: DownloadFunctionParams): Promise => { const cookies = new Cookies(); /* Create a cookie with a unique identifier to attach to request. Response from server will use the set-cookie header @@ -168,7 +175,7 @@ const download = async ({ downloadCookieKey: cookieKey, downloadCookiePath: '/', attachment: true, - ...(params.filename !== undefined? { download: params.filename } : {}), + ...(params.filename !== undefined ? { download: params.filename } : {}), }, (result, value, key) => { const paramValue = processParamObj(key, value); @@ -255,7 +262,7 @@ const download = async ({ const form = document.createElement('form'); form.method = method.toUpperCase(); form.action = endpoint; - form.innerHTML = fields; + form.innerText = fields; getBody(iFrame).appendChild(form); From fbc06bed9f11e6b251abba11e1f99146e6535a7c Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Fri, 9 Feb 2024 17:31:36 -0600 Subject: [PATCH 05/21] More download options --- package-lock.json | 72 ------- packages/core/src/features/guppy/types.ts | 1 + packages/core/src/features/guppy/utils.ts | 15 +- .../CohortBuilder/DownloadButtons.tsx | 199 +++++++++++++----- .../features/CohortBuilder/DownloadsPanel.tsx | 13 ++ .../features/CohortBuilder/buttonActions.tsx | 2 + packages/frontend/src/utils/download.tsx | 2 +- packages/sampleCommons/next.config.js | 4 + 8 files changed, 175 insertions(+), 133 deletions(-) diff --git a/package-lock.json b/package-lock.json index e628a437..e8678815 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7830,10 +7830,6 @@ "node": ">= 10" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "license": "MIT" - }, "node_modules/@trysound/sax": { "version": "0.2.0", "license": "ISC", @@ -32190,27 +32186,6 @@ "typescript": "5.0.2" } }, - "packages/crosswalk/node_modules/rollup-plugin-dts": { - "version": "5.3.1", - "dev": true, - "license": "LGPL-3.0", - "dependencies": { - "magic-string": "^0.30.2" - }, - "engines": { - "node": ">=v14.21.3" - }, - "funding": { - "url": "https://github.com/sponsors/Swatinem" - }, - "optionalDependencies": { - "@babel/code-frame": "^7.22.5" - }, - "peerDependencies": { - "rollup": "^3.0", - "typescript": "^4.1 || ^5.0" - } - }, "packages/frontend": { "name": "@gen3/frontend", "version": "0.20.2", @@ -32488,53 +32463,6 @@ "url": "https://opencollective.com/unified" } }, - "packages/sampleCommons/node_modules/@next/env": { - "version": "13.4.19", - "dev": true, - "license": "MIT" - }, - "packages/sampleCommons/node_modules/@next/mdx": { - "version": "14.0.1", - "license": "MIT", - "dependencies": { - "source-map": "^0.7.0" - }, - "peerDependencies": { - "@mdx-js/loader": ">=0.15.0", - "@mdx-js/react": ">=0.15.0" - }, - "peerDependenciesMeta": { - "@mdx-js/loader": { - "optional": true - }, - "@mdx-js/react": { - "optional": true - } - } - }, - "packages/sampleCommons/node_modules/@next/swc-darwin-arm64": { - "version": "13.4.19", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "packages/sampleCommons/node_modules/@swc/helpers": { - "version": "0.5.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.4.0" - } - }, "packages/sampleCommons/node_modules/@types/hast": { "version": "3.0.2", "license": "MIT", diff --git a/packages/core/src/features/guppy/types.ts b/packages/core/src/features/guppy/types.ts index 932f805c..cfbd3510 100644 --- a/packages/core/src/features/guppy/types.ts +++ b/packages/core/src/features/guppy/types.ts @@ -27,6 +27,7 @@ export interface GuppyActionParams> { onStart?: () => void; onDone?: (blob: Blob) => void; onError?: (error: Error) => void; + signal?: AbortSignal; } export interface GuppyDownloadActionFunctionParams diff --git a/packages/core/src/features/guppy/utils.ts b/packages/core/src/features/guppy/utils.ts index a2f764dd..4598ae4a 100644 --- a/packages/core/src/features/guppy/utils.ts +++ b/packages/core/src/features/guppy/utils.ts @@ -67,24 +67,28 @@ const prepareFetchConfig = ( * @param onStart - The function to call when the download starts. * @param onDone - The function to call when the download is done. * @param onError - The function to call when the download fails. + * @param signal - AbortSignal to use for the request. */ -export const downloadFromGuppy = ({ +export const downloadFromGuppy = async ({ parameters, onStart = () => null, onDone = (_: Blob) => null, onError = (_: Error) => null, + signal = undefined }: DownloadFromGuppyOptions) => { const csrfToken = selectCSRFToken(coreStore.getState()); onStart?.(); const url = prepareUrl(GEN3_GUPPY_API); const fetchConfig = prepareFetchConfig(parameters, csrfToken); + console.log('signal', signal); - fetch(url.toString(), fetchConfig) + fetch(url.toString(), {...fetchConfig, ...(signal ? { signal } : {})} as RequestInit) .then(async (response: Response) => { if (!response.ok) { throw new Error(response.statusText); } + let jsonData = await response.json(); if (parameters?.rootPath && parameters.rootPath) { // if rootPath is provided, extract the data from the rootPath jsonData = JSONPath({ @@ -109,5 +113,10 @@ export const downloadFromGuppy = ({ return new Blob([bytes]); }) .then((blob) => onDone?.(blob)) - .catch((error) => onError?.(error)); + .catch((error) => { + if (error.name == 'AbortError') { // handle abort() + console.log("Download Aborted"); + } + onError?.(error); + }); }; diff --git a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx index 9c2ca780..d3e2353b 100644 --- a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx +++ b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx @@ -1,6 +1,6 @@ import { Accessibility, - type GuppyDownloadActionFunctionParams, + type GuppyDownloadActionFunctionParams, hideModal, Modals, showModal, @@ -12,11 +12,15 @@ import { Button, Loader, Tooltip } from '@mantine/core'; import { FiDownload } from 'react-icons/fi'; import { useDeepCompareCallback } from 'use-deep-compare'; import { partial } from 'lodash'; +import { cleanNotifications, showNotification } from '@mantine/notifications'; +import { DownloadNotification } from '../../utils/download'; +import { useCallback } from 'react'; type ActionButtonFunction = ( done?: () => void, - onError?: (error: Error) => void) => void; - + onError?: (error: Error) => void, + signal?: AbortSignal, +) => void; interface GuppyActionButtonProps { disabled?: boolean; @@ -32,57 +36,123 @@ interface GuppyActionButtonProps { Modal403?: Modals; Modal400?: Modals; toolTip?: string; + done?: () => void; actionFunction: ActionButtonFunction; customErrorMessage?: string; + hideNotification?: boolean; } - -const GuppyActionButton = ( - { - active, - activeText, - inactiveText, - customStyle, - showLoading = true, - showIcon = true, - preventClickEvent = false, - onClick, - setActive, - disabled = false, - Modal403 = Modals.NoAccessModal, - Modal400 = Modals.GeneralErrorModal, - toolTip, - customErrorMessage, - actionFunction, - }: GuppyActionButtonProps, -) => { +const GuppyActionButton = ({ + active, + activeText, + inactiveText, + customStyle, + showLoading = true, + showIcon = true, + preventClickEvent = false, + onClick, + setActive, + disabled = false, + Modal403 = Modals.NoAccessModal, + Modal400 = Modals.GeneralErrorModal, + toolTip, + done, + customErrorMessage, + hideNotification = false, + actionFunction, +}: GuppyActionButtonProps) => { const text = active ? activeText : inactiveText; const ref = useRef(null); const dispatch = useCoreDispatch(); + const controller = new AbortController(); - const handleError = useDeepCompareCallback((error: Error) => { - const errorMessage: string = error.message; - if ( - errorMessage === 'internal server error' || - errorMessage === undefined - ) { - dispatch(showModal({ modal: Modal400, message: errorMessage })); - } else if ( - errorMessage === - 'Your token is invalid or expired. Please get a new token.' - ) { - dispatch(showModal({ modal: Modal403, message: errorMessage })); - } else { - dispatch( - showModal({ - modal: Modal400, - message: customErrorMessage || errorMessage, - }), - ); - } - }, [Modal400, Modal403, customErrorMessage, dispatch]); + const handleError = useDeepCompareCallback( + (error: Error) => { + const errorMessage: string = error.message; + if ( + errorMessage === 'internal server error' || + errorMessage === undefined + ) { + dispatch(showModal({ modal: Modal400, message: errorMessage })); + } else if ( + errorMessage === + 'Your token is invalid or expired. Please get a new token.' + ) { + dispatch(showModal({ modal: Modal403, message: errorMessage })); + } else { + dispatch( + showModal({ + modal: Modal400, + message: customErrorMessage || errorMessage, + }), + ); + } + }, + [Modal400, Modal403, customErrorMessage, dispatch], + ); + + const TimeoutFunction = useCallback(() => { + showNotification({ + message: ( + { + controller.abort(); + console.log('aborted'); + cleanNotifications(); + if (done) { + done(); + } + }} + /> + ), + styles: () => ({ + root: { + textAlign: 'center', + display: hideNotification ? 'none' : 'block', + }, + closeButton: { + color: 'black', + '&:hover': { + backgroundColor: 'lightslategray', + }, + }, + }), + closeButtonProps: { 'aria-label': 'Close notification' }, + }); + }, [controller, done, hideNotification] ); + const showNotificationTimeout = setTimeout( + () => + showNotification({ + message: ( + { + controller.abort(); + console.log('aborted'); + cleanNotifications(); + if (done) { + done(); + } + }} + /> + ), + styles: () => ({ + root: { + textAlign: 'center', + display: hideNotification ? 'none' : 'block', + }, + closeButton: { + color: 'black', + '&:hover': { + backgroundColor: 'lightslategray', + }, + }, + }), + closeButtonProps: { 'aria-label': 'Close notification' }, + }), + 100, + ); // set to 100 as that is perceived as instant const Icon = active ? ( @@ -110,11 +180,17 @@ const GuppyActionButton = ( dispatch(hideModal()); setActive && setActive(true); actionFunction( - () => setActive && setActive(false), + () => { + clearTimeout(showNotificationTimeout); + setActive && setActive(false); + }, (error: Error) => { + clearTimeout(showNotificationTimeout); handleError(error); setActive && setActive(false); - }); + }, + controller.signal, + ); }} > {text || Icon} @@ -123,20 +199,30 @@ const GuppyActionButton = ( ); }; +interface DownloadToFileButtonProps extends GuppyDownloadActionFunctionParams { + activeText: string; + inactiveText: string; + tooltipText: string; + rootPath?: string; +} export const DownloadToFileButton = ({ - type, - filename, - filter, - fields, - accessibility, - }: GuppyDownloadActionFunctionParams) => { + type, + filename, + filter, + fields, + accessibility, + activeText = 'Downloading...', + inactiveText = 'Download', + tooltipText = 'Download data', + rootPath = undefined, +}: GuppyDownloadActionFunctionParams) => { const [downloading, setDownloading] = useState(false); return ( ); - }; diff --git a/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx b/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx index 95b68056..590a0450 100644 --- a/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx +++ b/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx @@ -8,6 +8,7 @@ import { import ActionButton from '../../components/Buttons/ActionButton'; import { FilterSet, useIsUserLoggedIn, convertFilterSetToGqlFilter } from '@gen3/core'; import { DownloadToFileButton } from './DownloadButtons'; +import { DownloadTestJSONButton } from './testButton'; @@ -71,6 +72,18 @@ const DownloadsPanel = ({ format="json" /> + + {Object.values(dropdownsToRender).map((dropdown) => ( ))} diff --git a/packages/frontend/src/features/CohortBuilder/buttonActions.tsx b/packages/frontend/src/features/CohortBuilder/buttonActions.tsx index 6284f895..99a60407 100644 --- a/packages/frontend/src/features/CohortBuilder/buttonActions.tsx +++ b/packages/frontend/src/features/CohortBuilder/buttonActions.tsx @@ -26,6 +26,7 @@ export const downloadToFileAction = async ( params: DownloadFileFromGuppyParams, done?: () => void, onError?: (error: Error) => void, + signal?: AbortSignal, ): Promise => { const handleData = (data: Blob) => { handleDownload(data, params.filename); @@ -38,5 +39,6 @@ export const downloadToFileAction = async ( onError: (error: Error) => { onError && onError(error); }, + signal, }); }; diff --git a/packages/frontend/src/utils/download.tsx b/packages/frontend/src/utils/download.tsx index e07f47ed..7b2db366 100644 --- a/packages/frontend/src/utils/download.tsx +++ b/packages/frontend/src/utils/download.tsx @@ -262,7 +262,7 @@ const download = async = Record>({ const form = document.createElement('form'); form.method = method.toUpperCase(); form.action = endpoint; - form.innerText = fields; + form.innerHTML = fields; getBody(iFrame).appendChild(form); diff --git a/packages/sampleCommons/next.config.js b/packages/sampleCommons/next.config.js index b27e287a..16cbcd1b 100644 --- a/packages/sampleCommons/next.config.js +++ b/packages/sampleCommons/next.config.js @@ -1,5 +1,9 @@ 'use strict'; +const dns = require("dns"); + +dns.setDefaultResultOrder("ipv4first"); + // eslint-disable-next-line @typescript-eslint/no-var-requires require('./src/lib/plugins/index.js'); From e8ce30acd8e5568a89856a8e8b1de463625d9c90 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Fri, 9 Feb 2024 23:58:48 -0600 Subject: [PATCH 06/21] More download options --- packages/core/src/features/guppy/types.ts | 1 + packages/core/src/features/guppy/utils.ts | 6 ++- .../CohortBuilder/DownloadButtons.tsx | 53 +++++-------------- .../features/CohortBuilder/buttonActions.tsx | 4 ++ 4 files changed, 23 insertions(+), 41 deletions(-) diff --git a/packages/core/src/features/guppy/types.ts b/packages/core/src/features/guppy/types.ts index cfbd3510..dadc59ba 100644 --- a/packages/core/src/features/guppy/types.ts +++ b/packages/core/src/features/guppy/types.ts @@ -27,6 +27,7 @@ export interface GuppyActionParams> { onStart?: () => void; onDone?: (blob: Blob) => void; onError?: (error: Error) => void; + onAbort?: () => void; signal?: AbortSignal; } diff --git a/packages/core/src/features/guppy/utils.ts b/packages/core/src/features/guppy/utils.ts index 4598ae4a..bb13aba6 100644 --- a/packages/core/src/features/guppy/utils.ts +++ b/packages/core/src/features/guppy/utils.ts @@ -67,6 +67,7 @@ const prepareFetchConfig = ( * @param onStart - The function to call when the download starts. * @param onDone - The function to call when the download is done. * @param onError - The function to call when the download fails. + * @param onAbort - The function to call when the download is aborted. * @param signal - AbortSignal to use for the request. */ export const downloadFromGuppy = async ({ @@ -74,6 +75,7 @@ export const downloadFromGuppy = async ({ onStart = () => null, onDone = (_: Blob) => null, onError = (_: Error) => null, + onAbort = () => null, signal = undefined }: DownloadFromGuppyOptions) => { const csrfToken = selectCSRFToken(coreStore.getState()); @@ -83,7 +85,7 @@ export const downloadFromGuppy = async ({ const fetchConfig = prepareFetchConfig(parameters, csrfToken); console.log('signal', signal); - fetch(url.toString(), {...fetchConfig, ...(signal ? { signal } : {})} as RequestInit) + fetch(url.toString(), {...fetchConfig, ...(signal ? { signal: signal } : {})} as RequestInit) .then(async (response: Response) => { if (!response.ok) { throw new Error(response.statusText); @@ -115,7 +117,7 @@ export const downloadFromGuppy = async ({ .then((blob) => onDone?.(blob)) .catch((error) => { if (error.name == 'AbortError') { // handle abort() - console.log("Download Aborted"); + onAbort?.(); } onError?.(error); }); diff --git a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx index d3e2353b..44b317b4 100644 --- a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx +++ b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx @@ -6,7 +6,7 @@ import { showModal, useCoreDispatch, } from '@gen3/core'; -import { Dispatch, SetStateAction, useRef, useState } from 'react'; +import { Dispatch, SetStateAction, useRef, useMemo, useState } from 'react'; import { downloadToFileAction } from './buttonActions'; import { Button, Loader, Tooltip } from '@mantine/core'; import { FiDownload } from 'react-icons/fi'; @@ -19,6 +19,7 @@ import { useCallback } from 'react'; type ActionButtonFunction = ( done?: () => void, onError?: (error: Error) => void, + onAbort?: () => void, signal?: AbortSignal, ) => void; @@ -65,7 +66,7 @@ const GuppyActionButton = ({ const ref = useRef(null); const dispatch = useCoreDispatch(); - const controller = new AbortController(); + const handleError = useDeepCompareCallback( (error: Error) => { @@ -92,13 +93,12 @@ const GuppyActionButton = ({ [Modal400, Modal403, customErrorMessage, dispatch], ); - const TimeoutFunction = useCallback(() => { + const showDownloadNotification = useCallback((controller: AbortController) => { showNotification({ message: ( { controller.abort(); - console.log('aborted'); cleanNotifications(); if (done) { done(); @@ -120,39 +120,7 @@ const GuppyActionButton = ({ }), closeButtonProps: { 'aria-label': 'Close notification' }, }); - }, [controller, done, hideNotification] ); - - const showNotificationTimeout = setTimeout( - () => - showNotification({ - message: ( - { - controller.abort(); - console.log('aborted'); - cleanNotifications(); - if (done) { - done(); - } - }} - /> - ), - styles: () => ({ - root: { - textAlign: 'center', - display: hideNotification ? 'none' : 'block', - }, - closeButton: { - color: 'black', - '&:hover': { - backgroundColor: 'lightslategray', - }, - }, - }), - closeButtonProps: { 'aria-label': 'Close notification' }, - }), - 100, - ); // set to 100 as that is perceived as instant + }, [done, hideNotification] ); const Icon = active ? ( @@ -177,17 +145,24 @@ const GuppyActionButton = ({ onClick(); return; } + const controller = new AbortController(); + + showDownloadNotification(controller); dispatch(hideModal()); setActive && setActive(true); actionFunction( () => { - clearTimeout(showNotificationTimeout); setActive && setActive(false); + cleanNotifications(); }, (error: Error) => { - clearTimeout(showNotificationTimeout); handleError(error); setActive && setActive(false); + cleanNotifications(); + }, + () => { + setActive && setActive(false); + cleanNotifications(); }, controller.signal, ); diff --git a/packages/frontend/src/features/CohortBuilder/buttonActions.tsx b/packages/frontend/src/features/CohortBuilder/buttonActions.tsx index 99a60407..bc1be9a8 100644 --- a/packages/frontend/src/features/CohortBuilder/buttonActions.tsx +++ b/packages/frontend/src/features/CohortBuilder/buttonActions.tsx @@ -26,6 +26,7 @@ export const downloadToFileAction = async ( params: DownloadFileFromGuppyParams, done?: () => void, onError?: (error: Error) => void, + onAbort?: () => void, signal?: AbortSignal, ): Promise => { const handleData = (data: Blob) => { @@ -39,6 +40,9 @@ export const downloadToFileAction = async ( onError: (error: Error) => { onError && onError(error); }, + onAbort: () => { + onAbort && onAbort(); + }, signal, }); }; From 5e3563db147758e3836b2a49a846ba94f4bdc580 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Sun, 11 Feb 2024 16:29:38 -0600 Subject: [PATCH 07/21] Refine download Button and Actions --- packages/core/src/features/guppy/types.ts | 27 ++-- packages/core/src/features/guppy/utils.ts | 12 +- packages/frontend/package.json | 2 +- .../DownloadButtons/DownloadButton.tsx | 3 +- .../{ => Buttons}/DownloadButtons/index.ts | 0 .../CohortBuilder/DownloadButtons.tsx | 123 ++++++------------ .../features/CohortBuilder/DownloadsPanel.tsx | 50 ++++--- .../{ => actions}/buttonActions.tsx | 23 +--- 8 files changed, 87 insertions(+), 153 deletions(-) rename packages/frontend/src/components/{ => Buttons}/DownloadButtons/DownloadButton.tsx (98%) rename packages/frontend/src/components/{ => Buttons}/DownloadButtons/index.ts (100%) rename packages/frontend/src/features/CohortBuilder/{ => actions}/buttonActions.tsx (69%) diff --git a/packages/core/src/features/guppy/types.ts b/packages/core/src/features/guppy/types.ts index dadc59ba..d855ee47 100644 --- a/packages/core/src/features/guppy/types.ts +++ b/packages/core/src/features/guppy/types.ts @@ -1,6 +1,8 @@ import { FilterSet } from '../filters'; import { Accessibility } from '../../constants'; + +// Guppy data request parameters export interface BaseGuppyDataRequest { type: string; accessibility?: Accessibility; @@ -8,6 +10,7 @@ export interface BaseGuppyDataRequest { sort?: string[]; } +// Represents a request to download data from Guppy and convert it to a specific format. export interface GuppyDownloadDataParams extends BaseGuppyDataRequest { filter: FilterSet; // cohort filters format: 'json' | 'csv' | 'tsv'; // the three supported formats @@ -23,20 +26,20 @@ export interface GuppyActionFunctionParams extends Record { } export interface GuppyActionParams> { - parameters: T; - onStart?: () => void; - onDone?: (blob: Blob) => void; - onError?: (error: Error) => void; - onAbort?: () => void; - signal?: AbortSignal; + parameters: T; // query parameters for the Guppy request + onStart?: () => void; // function to call when the download starts + onDone?: (blob: Blob) => void; // function to call when the download is done + onError?: (error: Error) => void; // function to call when the download fails + onAbort?: () => void; // function to call when the download is aborted + signal?: AbortSignal; // AbortSignal to use for the request } -export interface GuppyDownloadActionFunctionParams - extends GuppyActionFunctionParams { - format: string; +export interface GuppyDownloadActionFunctionParams extends GuppyDownloadDataParams { filename: string; } -export type GuppyActionFunction> = ( - params: GuppyActionParams, -) => void; +// Function type for Guppy actions +export type GuppyActionFunction> = (args:GuppyActionParams) => void; + + +export type DownloadFromGuppyParams = GuppyActionParams; diff --git a/packages/core/src/features/guppy/utils.ts b/packages/core/src/features/guppy/utils.ts index bb13aba6..28209e94 100644 --- a/packages/core/src/features/guppy/utils.ts +++ b/packages/core/src/features/guppy/utils.ts @@ -1,4 +1,4 @@ -import { GuppyActionParams, GuppyDownloadDataParams } from './types'; +import { DownloadFromGuppyParams, GuppyDownloadDataParams } from './types'; import { GEN3_GUPPY_API } from '../../constants'; import { selectCSRFToken } from '../gen3'; import { coreStore } from '../../store'; @@ -7,9 +7,6 @@ import { jsonToFormat } from './conversion'; import { isJSONObject } from '../../types'; import { JSONPath } from 'jsonpath-plus'; -export type DownloadFromGuppyOptions = - GuppyActionParams; - /** * Represents a configuration for making a fetch request. * @@ -63,7 +60,7 @@ const prepareFetchConfig = ( * Downloads a file from Guppy using the provided parameters. * It will optionally convert the data to the specified format. * - * @param {DownloadFromGuppyOptions} parameters - The parameters to use for the download request. + * @param {DownloadFromGuppyParams} parameters - The parameters to use for the download request. * @param onStart - The function to call when the download starts. * @param onDone - The function to call when the download is done. * @param onError - The function to call when the download fails. @@ -77,13 +74,12 @@ export const downloadFromGuppy = async ({ onError = (_: Error) => null, onAbort = () => null, signal = undefined -}: DownloadFromGuppyOptions) => { +}: DownloadFromGuppyParams) => { const csrfToken = selectCSRFToken(coreStore.getState()); onStart?.(); const url = prepareUrl(GEN3_GUPPY_API); const fetchConfig = prepareFetchConfig(parameters, csrfToken); - console.log('signal', signal); fetch(url.toString(), {...fetchConfig, ...(signal ? { signal: signal } : {})} as RequestInit) .then(async (response: Response) => { @@ -115,7 +111,7 @@ export const downloadFromGuppy = async ({ return new Blob([bytes]); }) .then((blob) => onDone?.(blob)) - .catch((error) => { + .catch((error) => { // Abort is handle as an exception if (error.name == 'AbortError') { // handle abort() onAbort?.(); } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index ab6d641b..e1015af9 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -11,7 +11,7 @@ "author": "", "license": "Apache-2.0", "sideEffects": false, - "main": "dist/index.js", + "main": "dist/index.esm.js", "module": "dist/index.esm.js", "unpkg": "dist/index.umd.js", "types": "dist/index.d.ts", diff --git a/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx b/packages/frontend/src/components/Buttons/DownloadButtons/DownloadButton.tsx similarity index 98% rename from packages/frontend/src/components/DownloadButtons/DownloadButton.tsx rename to packages/frontend/src/components/Buttons/DownloadButtons/DownloadButton.tsx index 5b4af084..0782140a 100644 --- a/packages/frontend/src/components/DownloadButtons/DownloadButton.tsx +++ b/packages/frontend/src/components/Buttons/DownloadButtons/DownloadButton.tsx @@ -1,6 +1,6 @@ import { Button, ButtonProps, Loader, Tooltip } from '@mantine/core'; import { FiDownload } from 'react-icons/fi'; -import download, { DownloadFunctionParams } from '../../utils/download'; +import download, { DownloadFunctionParams } from '../../../utils/download'; import { hideModal, Modals, useCoreDispatch } from '@gen3/core'; import { Dispatch, SetStateAction, forwardRef } from 'react'; @@ -76,7 +76,6 @@ interface DownloadButtonProps { * @category Buttons */ - export const DownloadButton = forwardRef< HTMLButtonElement, DownloadButtonProps & ButtonProps diff --git a/packages/frontend/src/components/DownloadButtons/index.ts b/packages/frontend/src/components/Buttons/DownloadButtons/index.ts similarity index 100% rename from packages/frontend/src/components/DownloadButtons/index.ts rename to packages/frontend/src/components/Buttons/DownloadButtons/index.ts diff --git a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx index 44b317b4..408afdda 100644 --- a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx +++ b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx @@ -1,17 +1,8 @@ -import { - Accessibility, - type GuppyDownloadActionFunctionParams, - hideModal, - Modals, - showModal, - useCoreDispatch, -} from '@gen3/core'; +import { hideModal, Modals, showModal, useCoreDispatch } from '@gen3/core'; import { Dispatch, SetStateAction, useRef, useMemo, useState } from 'react'; -import { downloadToFileAction } from './buttonActions'; import { Button, Loader, Tooltip } from '@mantine/core'; import { FiDownload } from 'react-icons/fi'; import { useDeepCompareCallback } from 'use-deep-compare'; -import { partial } from 'lodash'; import { cleanNotifications, showNotification } from '@mantine/notifications'; import { DownloadNotification } from '../../utils/download'; import { useCallback } from 'react'; @@ -21,7 +12,7 @@ type ActionButtonFunction = ( onError?: (error: Error) => void, onAbort?: () => void, signal?: AbortSignal, -) => void; +) => Promise; interface GuppyActionButtonProps { disabled?: boolean; @@ -43,8 +34,7 @@ interface GuppyActionButtonProps { hideNotification?: boolean; } -const GuppyActionButton = ({ - active, +export const GuppyActionButton = ({ activeText, inactiveText, customStyle, @@ -52,7 +42,6 @@ const GuppyActionButton = ({ showIcon = true, preventClickEvent = false, onClick, - setActive, disabled = false, Modal403 = Modals.NoAccessModal, Modal400 = Modals.GeneralErrorModal, @@ -62,12 +51,11 @@ const GuppyActionButton = ({ hideNotification = false, actionFunction, }: GuppyActionButtonProps) => { + const [active, setActive] = useState(false); const text = active ? activeText : inactiveText; const ref = useRef(null); const dispatch = useCoreDispatch(); - - const handleError = useDeepCompareCallback( (error: Error) => { const errorMessage: string = error.message; @@ -93,34 +81,37 @@ const GuppyActionButton = ({ [Modal400, Modal403, customErrorMessage, dispatch], ); - const showDownloadNotification = useCallback((controller: AbortController) => { - showNotification({ - message: ( - { - controller.abort(); - cleanNotifications(); - if (done) { - done(); - } - }} - /> - ), - styles: () => ({ - root: { - textAlign: 'center', - display: hideNotification ? 'none' : 'block', - }, - closeButton: { - color: 'black', - '&:hover': { - backgroundColor: 'lightslategray', + const showDownloadNotification = useCallback( + (controller: AbortController) => { + showNotification({ + message: ( + { + controller.abort(); + cleanNotifications(); + if (done) { + done(); + } + }} + /> + ), + styles: () => ({ + root: { + textAlign: 'center', + display: hideNotification ? 'none' : 'block', + }, + closeButton: { + color: 'black', + '&:hover': { + backgroundColor: 'lightslategray', + }, }, - }, - }), - closeButtonProps: { 'aria-label': 'Close notification' }, - }); - }, [done, hideNotification] ); + }), + closeButtonProps: { 'aria-label': 'Close notification' }, + }); + }, + [done, hideNotification], + ); const Icon = active ? ( @@ -140,17 +131,17 @@ const GuppyActionButton = ({ } ` } loading={showLoading && active} - onClick={() => { + onClick={async () => { if (!preventClickEvent && onClick) { onClick(); return; } - const controller = new AbortController(); + const controller = new AbortController(); showDownloadNotification(controller); dispatch(hideModal()); setActive && setActive(true); - actionFunction( + await actionFunction( () => { setActive && setActive(false); cleanNotifications(); @@ -173,43 +164,3 @@ const GuppyActionButton = ({ ); }; - -interface DownloadToFileButtonProps extends GuppyDownloadActionFunctionParams { - activeText: string; - inactiveText: string; - tooltipText: string; - rootPath?: string; -} - -export const DownloadToFileButton = ({ - type, - filename, - filter, - fields, - accessibility, - activeText = 'Downloading...', - inactiveText = 'Download', - tooltipText = 'Download data', - rootPath = undefined, -}: GuppyDownloadActionFunctionParams) => { - const [downloading, setDownloading] = useState(false); - - return ( - - ); -}; diff --git a/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx b/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx index 590a0450..b32ff952 100644 --- a/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx +++ b/packages/frontend/src/features/CohortBuilder/DownloadsPanel.tsx @@ -6,16 +6,17 @@ import { DownloadButtonProps, } from '../../components/Buttons/DropdownButtons'; import ActionButton from '../../components/Buttons/ActionButton'; -import { FilterSet, useIsUserLoggedIn, convertFilterSetToGqlFilter } from '@gen3/core'; -import { DownloadToFileButton } from './DownloadButtons'; -import { DownloadTestJSONButton } from './testButton'; - - +import { FilterSet, useIsUserLoggedIn, Accessibility } from '@gen3/core'; +import { GuppyActionButton } from './DownloadButtons'; +import { partial } from 'lodash'; +import { downloadToFileAction } from './actions/buttonActions'; interface DownloadsPanelProps { dropdowns: Record; buttons: DownloadButtonProps[]; loginForDownload?: boolean; + accessibility?: Accessibility; + rootPath?: string; index: string; totalCount: number; fields: string[]; @@ -30,12 +31,13 @@ const DownloadsPanel = ({ totalCount, fields, filters, + accessibility, + rootPath, }: DownloadsPanelProps): JSX.Element => { const isUserLoggedIn = useIsUserLoggedIn(); const loginRequired = loginForDownload ? loginForDownload : false; const dropdownsToRender = useDeepCompareMemo(() => { - let dropdownsToRender = dropdowns; if (loginRequired && !isUserLoggedIn) { @@ -62,35 +64,27 @@ const DownloadsPanel = ({ return dropdowns ? (
+ - - - {Object.values(dropdownsToRender).map((dropdown) => ( ))} {buttons.map((button) => ( ))} -
) : ( diff --git a/packages/frontend/src/features/CohortBuilder/buttonActions.tsx b/packages/frontend/src/features/CohortBuilder/actions/buttonActions.tsx similarity index 69% rename from packages/frontend/src/features/CohortBuilder/buttonActions.tsx rename to packages/frontend/src/features/CohortBuilder/actions/buttonActions.tsx index bc1be9a8..dbcdaa6e 100644 --- a/packages/frontend/src/features/CohortBuilder/buttonActions.tsx +++ b/packages/frontend/src/features/CohortBuilder/actions/buttonActions.tsx @@ -1,17 +1,9 @@ -import { - GuppyDownloadDataParams, - downloadFromGuppy, -} from '@gen3/core'; +import { GuppyDownloadDataParams, downloadFromGuppy } from '@gen3/core'; interface DownloadFileFromGuppyParams extends GuppyDownloadDataParams { filename: string; } -export interface DownloadActionParams { - done?: () => void; - onError?: (error: Error) => void; -} - const handleDownload = (data: Blob, filename: string) => { const aElement = document.createElement('a'); const href = URL.createObjectURL(data); @@ -29,14 +21,13 @@ export const downloadToFileAction = async ( onAbort?: () => void, signal?: AbortSignal, ): Promise => { - const handleData = (data: Blob) => { - handleDownload(data, params.filename); - done && done(); - }; - - downloadFromGuppy({ + // call the downloadFromGuppy function + await downloadFromGuppy({ parameters: params, - onDone: (data: Blob) => handleData(data), + onDone: (data: Blob) => { + handleDownload(data, params.filename); + done && done(); + }, onError: (error: Error) => { onError && onError(error); }, From 4280ace05518dd82e9b5f489b2faba8e59dd6fc9 Mon Sep 17 00:00:00 2001 From: craigrbarnes Date: Mon, 12 Feb 2024 15:19:22 -0600 Subject: [PATCH 08/21] add guppy action registry --- .../Buttons/DropdownButtons/types.ts | 3 +- .../CohortBuilder/DownloadButtons.tsx | 17 ++---- .../features/CohortBuilder/DownloadsPanel.tsx | 54 ++++++++++++------- .../{buttonActions.tsx => downloadToFile.tsx} | 4 +- .../actions/registeredActions.ts | 45 ++++++++++++++++ .../src/features/CohortBuilder/types.ts | 15 ++++++ 6 files changed, 104 insertions(+), 34 deletions(-) rename packages/frontend/src/features/CohortBuilder/actions/{buttonActions.tsx => downloadToFile.tsx} (92%) create mode 100644 packages/frontend/src/features/CohortBuilder/actions/registeredActions.ts diff --git a/packages/frontend/src/components/Buttons/DropdownButtons/types.ts b/packages/frontend/src/components/Buttons/DropdownButtons/types.ts index c1a96699..08e78e3a 100644 --- a/packages/frontend/src/components/Buttons/DropdownButtons/types.ts +++ b/packages/frontend/src/components/Buttons/DropdownButtons/types.ts @@ -4,8 +4,9 @@ export interface DownloadButtonProps { title: string; leftIcon?: string; rightIcon?: string; - fileName: string; tooltipText?: string; + action?: string; + actionArgs?: Record; } export interface DropdownButtonsProps { diff --git a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx index 408afdda..b2173689 100644 --- a/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx +++ b/packages/frontend/src/features/CohortBuilder/DownloadButtons.tsx @@ -1,18 +1,11 @@ import { hideModal, Modals, showModal, useCoreDispatch } from '@gen3/core'; -import { Dispatch, SetStateAction, useRef, useMemo, useState } from 'react'; +import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'; import { Button, Loader, Tooltip } from '@mantine/core'; import { FiDownload } from 'react-icons/fi'; import { useDeepCompareCallback } from 'use-deep-compare'; import { cleanNotifications, showNotification } from '@mantine/notifications'; import { DownloadNotification } from '../../utils/download'; -import { useCallback } from 'react'; - -type ActionButtonFunction = ( - done?: () => void, - onError?: (error: Error) => void, - onAbort?: () => void, - signal?: AbortSignal, -) => Promise; +import { ActionButtonFunction } from './types'; interface GuppyActionButtonProps { disabled?: boolean; @@ -27,7 +20,7 @@ interface GuppyActionButtonProps { active?: boolean; Modal403?: Modals; Modal400?: Modals; - toolTip?: string; + tooltipText?: string; done?: () => void; actionFunction: ActionButtonFunction; customErrorMessage?: string; @@ -45,7 +38,7 @@ export const GuppyActionButton = ({ disabled = false, Modal403 = Modals.NoAccessModal, Modal400 = Modals.GeneralErrorModal, - toolTip, + tooltipText, done, customErrorMessage, hideNotification = false, @@ -119,7 +112,7 @@ export const GuppyActionButton = ({ ); return ( - + diff --git a/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx b/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx new file mode 100644 index 00000000..4cc34a93 --- /dev/null +++ b/packages/frontend/src/components/DropdownWithIcon/DropdownWithIcon.tsx @@ -0,0 +1,157 @@ +import { Button, Menu } from "@mantine/core"; +import { FloatingPosition } from "@mantine/core/lib/Floating/types"; +import { ReactNode } from "react"; +import { Tooltip } from "@mantine/core"; +import { IoMdArrowDropdown as Dropdown } from "react-icons/io"; +import { focusStyles } from "../../utils"; + +interface DropdownWithIconProps { + /** + * if true, doesn't set width to be "target" + */ + disableTargetWidth?: string; + /** + * Left Icon for the taret button, can be undefined too + */ + LeftIcon?: JSX.Element; + /** + * Right Icon for the taret button, can be undefined too (default to dropdown icon) + */ + RightIcon?: JSX.Element; + /** + * Content for target button + */ + TargetButtonChildren: ReactNode; + /** + * disables the target button and menu + */ + targetButtonDisabled?: boolean; + /** + * array dropdown items. Need to pass title, onClick and icon event handler is optional + */ + dropdownElements: Array<{ + title: string; + onClick?: () => void; + icon?: JSX.Element; + disabled?: boolean; // if true, disables the menu item + }>; + /** + * only provide menuLabelText if we want label for dropdown elements + */ + menuLabelText?: string; + /** + * custom class / stylings for menuLabelText + */ + menuLabelCustomClass?: string; + /** + * custom position for Menu + */ + customPosition?: FloatingPosition; + /** + * whether the dropdown should fill the height of its parent + */ + fullHeight?: boolean; + /** + * custom z-index for Menu, defaults to undefined + */ + zIndex?: number; + /** + * custom test id + */ + customDataTestId?: string; + + /** + tooltip + */ + tooltip?: string; + + /** + * aria-label for the button + */ + buttonAriaLabel?: string; +} + +export const DropdownWithIcon = ({ + disableTargetWidth, + LeftIcon, + RightIcon = ( +