From 7f76939440e492e334c6833aaad87954211d2b5b Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Mon, 3 Oct 2022 17:21:46 +0100 Subject: [PATCH 01/11] Download table data --- .../SingleBlastJobResult.tsx | 29 ++--- .../components/data-table/DataTable.tsx | 11 +- .../components/table-actions/TableActions.tsx | 5 + .../download-data/DownloadData.scss | 14 +++ .../components/download-data/DownloadData.tsx | 107 ++++++++++++++++++ .../components/data-table/dataTableTypes.ts | 1 + 6 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.scss create mode 100644 src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx diff --git a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx index 4efae466d4..6f7833486f 100644 --- a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx +++ b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx @@ -70,7 +70,12 @@ const hitsTableColumns: DataTableColumns = [ title: 'Length', isSortable: true }, - { width: '200px', columnId: 'view_alignment', isHideable: false }, + { + width: '200px', + columnId: 'view_alignment', + isHideable: false, + isExportable: false + }, { width: '100px', columnId: 'percentage_id', @@ -199,6 +204,8 @@ type HitsTableProps = { const HitsTable = (props: HitsTableProps) => { const { jobResult, blastDatabase } = props; + const { jobId } = jobResult; + const [tableState, setTableState] = useState>({ rowsPerPage: 100, sortedColumn: { @@ -229,12 +236,11 @@ const HitsTable = (props: HitsTableProps) => { '', // view_alignment hitHsp.hsp_identity, hitHsp.hsp_score, - , + getDynamicColumnContent({ + hit, + blastDatabase, + hitHsp + }), {hitHsp.hsp_hit_frame === '1' ? 'Forward' : 'Reverse'} , @@ -339,11 +345,8 @@ const HitsTable = (props: HitsTableProps) => { theme="dark" className={styles.hitsTable} expandedContent={expandedContent} - disabledActions={[ - TableAction.FILTERS, - TableAction.DOWNLOAD_ALL_DATA, - TableAction.DOWNLOAD_SHOWN_DATA - ]} + disabledActions={[TableAction.FILTERS]} + exportFileName={`ensembl-blast_${jobId}.csv`} /> ); @@ -373,7 +376,7 @@ type DynamicColumnContentProps = { blastDatabase: DatabaseType; }; -const DynamicColumnContent = (props: DynamicColumnContentProps) => { +const getDynamicColumnContent = (props: DynamicColumnContentProps) => { const { hit, blastDatabase, hitHsp } = props; if (blastDatabase !== 'dna') { diff --git a/src/shared/components/data-table/DataTable.tsx b/src/shared/components/data-table/DataTable.tsx index 2a92413036..7af28fce99 100644 --- a/src/shared/components/data-table/DataTable.tsx +++ b/src/shared/components/data-table/DataTable.tsx @@ -40,6 +40,7 @@ type TableContextType = DataTableState & { selectableColumnIndex: number; expandedContent: { [rowId: string]: ReactNode }; disabledActions?: TableAction[]; + exportFileName?: string; rows: TableRows; }; @@ -48,14 +49,15 @@ export const TableContext = React.createContext( ); export type TableProps = { - onStateChange?: (newState: DataTableState) => void; + state: Partial; columns: DataTableColumns; - state?: Partial; theme: TableTheme; selectableColumnIndex: number; - className?: string; expandedContent: { [rowId: string]: ReactNode }; disabledActions?: TableAction[]; + className?: string; + exportFileName?: string; + onStateChange?: (newState: DataTableState) => void; }; const DataTable = (props: TableProps) => { const initialDataTableState = { @@ -96,7 +98,8 @@ const DataTable = (props: TableProps) => { theme: props.theme, selectableColumnIndex: props.selectableColumnIndex, expandedContent: props.expandedContent, - disabledActions: props.disabledActions + disabledActions: props.disabledActions, + exportFileName: props.exportFileName }} >
diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/TableActions.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/TableActions.tsx index edb2c46761..1ee76a2faa 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/TableActions.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/TableActions.tsx @@ -21,6 +21,7 @@ import { TableContext } from 'src/shared/components/data-table/DataTable'; import RowVisibilityController from 'src/shared/components/data-table/components/main/components/table-row/components/row-visibility-controller/RowVisibilityController'; import FindInTable from './components/find-in-table/FindInTable'; import ShowHideColumns from './components/show-hide-columns/ShowHideColumns'; +import DownloadData from './components/download-data/DownloadData'; import { type DataTableState, @@ -144,6 +145,10 @@ const getActionComponent = (selectedAction: TableAction) => { return ; case TableAction.SHOW_HIDE_ROWS: return ; + case TableAction.DOWNLOAD_ALL_DATA: + return ; + case TableAction.DOWNLOAD_SHOWN_DATA: + return ; default: return null; } diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.scss b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.scss new file mode 100644 index 0000000000..27e4fffcda --- /dev/null +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.scss @@ -0,0 +1,14 @@ +@import 'src/styles/common'; + +.downloadData { + display: flex; + column-gap: 10px; + margin-left: 10px; + align-items: center; +} + +.cancel { + color: $blue; + cursor: pointer; + margin-left: 10px; +} diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx new file mode 100644 index 0000000000..d10c88bc6e --- /dev/null +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -0,0 +1,107 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { ReactNode } from 'react'; + +import { TableAction } from 'src/shared/components/data-table/dataTableTypes'; +import useDataTable from 'src/shared/components/data-table/hooks/useDataTable'; + +import { PrimaryButton } from 'src/shared/components/button/Button'; + +import styles from './DownloadData.scss'; +import { downloadTextAsFile } from 'src/shared/helpers/downloadAsFile'; + +const getReactNodeText = (node: ReactNode): string => { + if (['string', 'number'].includes(typeof node)) { + return node?.toString() || ''; + } + + if (typeof node === 'object' && node && 'props' in node) { + return getReactNodeText(node.props.children); + } + + return ''; +}; + +const DownloadData = () => { + const { + dispatch, + rows, + exportFileName, + columns, + selectedAction, + hiddenRowIds + } = useDataTable(); + + const onCancel = () => { + dispatch({ + type: 'set_selected_action', + payload: TableAction.DEFAULT + }); + }; + + const handleDownload = () => { + const dataForExport: string[][] = []; + dataForExport[0] = [ + ...columns + .filter((column) => column.isExportable !== false) + .map((column) => column.title ?? '') + ]; + + const rowsToDownload = + selectedAction === TableAction.DOWNLOAD_ALL_DATA + ? rows + : rows.filter((row) => !hiddenRowIds[row.rowId]); + + rowsToDownload.forEach((row, rowIndex) => { + dataForExport[rowIndex + 1] = []; + row.cells.forEach((cell, cellIndex) => { + const { renderer, isExportable } = columns[cellIndex]; + + if (isExportable !== false) { + const cellExportData = renderer + ? renderer({ + rowData: row.cells, + rowId: String(row.rowId), + cellData: cell + }) + : cell; + + dataForExport[rowIndex + 1].push(getReactNodeText(cellExportData)); + } + }); + }); + + const csv = formatCSV(dataForExport); + + downloadTextAsFile(csv, exportFileName ?? 'Table export.csv'); + }; + + return ( +
+ {exportFileName ?? 'Table export.csv'} + Download + + cancel + +
+ ); +}; + +const formatCSV = (table: (string | number)[][]) => { + return table.map((row) => row.join(',')).join('\n'); +}; + +export default DownloadData; diff --git a/src/shared/components/data-table/dataTableTypes.ts b/src/shared/components/data-table/dataTableTypes.ts index 51736ee41c..b4ae06b264 100644 --- a/src/shared/components/data-table/dataTableTypes.ts +++ b/src/shared/components/data-table/dataTableTypes.ts @@ -62,6 +62,7 @@ export type IndividualColumn = { isSearchable?: boolean; isFilterable?: boolean; isHideable?: boolean; + isExportable?: boolean; headerCellClassName?: string; bodyCellClassName?: string; helpText?: ReactNode; From 5db5cb93e69770219b0ef47d644fc0bd7cdc9d79 Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Tue, 4 Oct 2022 14:17:35 +0100 Subject: [PATCH 02/11] Let parent handle download --- .../blast-download/submissionDownload.ts | 2 +- .../BlastSubmissionResults.tsx | 3 +- .../BlastResultsPerSequence.tsx | 21 +++--- .../SingleBlastJobResult.tsx | 66 +++++++++++++++---- .../components/data-table/DataTable.tsx | 9 ++- .../components/table-actions/TableActions.tsx | 2 +- .../components/download-data/DownloadData.tsx | 13 ++-- 7 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/content/app/tools/blast/blast-download/submissionDownload.ts b/src/content/app/tools/blast/blast-download/submissionDownload.ts index 5fa82af513..f257115b63 100644 --- a/src/content/app/tools/blast/blast-download/submissionDownload.ts +++ b/src/content/app/tools/blast/blast-download/submissionDownload.ts @@ -168,7 +168,7 @@ const createZipArchive = async (submission: EnrichedBlastSubmission) => { return zip; }; -const getNameForZipRoot = (submission: BlastSubmission) => { +export const getNameForZipRoot = (submission: BlastSubmission) => { const { id, submittedData: { diff --git a/src/content/app/tools/blast/views/blast-submission-results/BlastSubmissionResults.tsx b/src/content/app/tools/blast/views/blast-submission-results/BlastSubmissionResults.tsx index f353548045..f1be2f0f2e 100644 --- a/src/content/app/tools/blast/views/blast-submission-results/BlastSubmissionResults.tsx +++ b/src/content/app/tools/blast/views/blast-submission-results/BlastSubmissionResults.tsx @@ -124,9 +124,8 @@ const Main = () => { key={data.sequence.id} species={data.species} sequence={data.sequence} - preset={blastSubmission.submittedData.preset} + submission={blastSubmission} blastResults={data.blastResults} - parameters={blastSubmission.submittedData.parameters} /> ) ); diff --git a/src/content/app/tools/blast/views/blast-submission-results/components/blast-results-per-sequence/BlastResultsPerSequence.tsx b/src/content/app/tools/blast/views/blast-submission-results/components/blast-results-per-sequence/BlastResultsPerSequence.tsx index c22441cdce..815cb33865 100644 --- a/src/content/app/tools/blast/views/blast-submission-results/components/blast-results-per-sequence/BlastResultsPerSequence.tsx +++ b/src/content/app/tools/blast/views/blast-submission-results/components/blast-results-per-sequence/BlastResultsPerSequence.tsx @@ -26,11 +26,10 @@ import SingleBlastJobResult from '../single-blast-job-result/SingleBlastJobResul import { parseBlastInput } from 'src/content/app/tools/blast/utils/blastInputParser'; import type { - BlastSubmissionParameters, - BlastJobWithResults + BlastJobWithResults, + BlastSubmission } from 'src/content/app/tools/blast/state/blast-results/blastResultsSlice'; import type { Species } from 'src/content/app/tools/blast/state/blast-form/blastFormSlice'; -import type { DatabaseType } from 'src/content/app/tools/blast/types/blastSettings'; import styles from './BlastResultsPerSequence.scss'; @@ -40,13 +39,19 @@ type BlastResultsPerSequenceProps = { value: string; }; species: Species[]; - preset: string; blastResults: BlastJobWithResults[]; - parameters: BlastSubmissionParameters; + submission: BlastSubmission; }; const BlastResultsPerSequence = (props: BlastResultsPerSequenceProps) => { - const { sequence, species, blastResults, parameters } = props; + const { + sequence, + species, + blastResults, + submission: { + submittedData: { parameters, preset } + } + } = props; const parsedBlastSequence = parseBlastInput(sequence.value)[0]; const { header: sequenceHeader, value: sequenceValue } = parsedBlastSequence; const sequenceHeaderLabel = @@ -73,7 +78,7 @@ const BlastResultsPerSequence = (props: BlastResultsPerSequenceProps) => { )}
@@ -103,7 +108,7 @@ const BlastResultsPerSequence = (props: BlastResultsPerSequenceProps) => { species={speciesInfo} jobResult={result} diagramWidth={plotwidth} - blastDatabase={parameters.database as DatabaseType} + submission={props.submission} /> ); })} diff --git a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx index 6f7833486f..a2ad933f2c 100644 --- a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx +++ b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx @@ -24,12 +24,19 @@ import ShowHide from 'src/shared/components/show-hide/ShowHide'; import BlastHitsDiagram from 'src/content/app/tools/blast/components/blast-hits-diagram/BlastHitsDiagram'; import BlastSequenceAlignment from 'src/content/app/tools/blast/components/blast-sequence-alignment/BlastSequenceAlignment'; +import { createCSVForGenomicBlast } from 'src/content/app/tools/blast/blast-download/createBlastCSVTable'; +import { getNameForZipRoot } from 'src/content/app/tools/blast/blast-download/submissionDownload'; +import { downloadBlobAsFile } from 'src/shared/helpers/downloadAsFile'; + import type { BlastHit, BlastJobResult, HSP } from 'src/content/app/tools/blast/types/blastJob'; -import type { BlastJobWithResults } from 'src/content/app/tools/blast/state/blast-results/blastResultsSlice'; +import type { + BlastJobWithResults, + BlastSubmission +} from 'src/content/app/tools/blast/state/blast-results/blastResultsSlice'; import type { Species } from 'src/content/app/tools/blast/state/blast-form/blastFormSlice'; import type { BlastSequenceAlignmentInput } from 'src/content/app/tools/blast/components/blast-sequence-alignment/blastSequenceAlignmentTypes'; import type { DatabaseType } from 'src/content/app/tools/blast/types/blastSettings'; @@ -48,7 +55,7 @@ type SingleBlastJobResultProps = { jobResult: BlastJobWithResults; species: Species; diagramWidth: number; - blastDatabase: DatabaseType; + submission: BlastSubmission; }; const hitsTableColumns: DataTableColumns = [ @@ -158,12 +165,7 @@ const hitsTableColumns: DataTableColumns = [ ]; const SingleBlastJobResult = (props: SingleBlastJobResultProps) => { - const { - species: speciesInfo, - jobResult, - diagramWidth, - blastDatabase - } = props; + const { species: speciesInfo, jobResult, diagramWidth, submission } = props; const [isExpanded, setExpanded] = useState(false); const alignmentsCount = countAlignments(jobResult.data); @@ -191,7 +193,11 @@ const SingleBlastJobResult = (props: SingleBlastJobResultProps) => { {isExpanded && ( - + )} ); @@ -199,12 +205,16 @@ const SingleBlastJobResult = (props: SingleBlastJobResultProps) => { type HitsTableProps = { jobResult: SingleBlastJobResultProps['jobResult']; - blastDatabase: DatabaseType; + submission: BlastSubmission; + species: Species; }; const HitsTable = (props: HitsTableProps) => { - const { jobResult, blastDatabase } = props; + const { jobResult, submission, species } = props; + + const blastDatabase = submission.submittedData.parameters + .database as DatabaseType; - const { jobId } = jobResult; + const zipFileName = getNameForZipRoot(submission) + '.zip'; const [tableState, setTableState] = useState>({ rowsPerPage: 100, @@ -336,6 +346,29 @@ const HitsTable = (props: HitsTableProps) => { ); }; + const downloadHandler = async () => { + const JSZip = await import('jszip').then((module) => module.default); // use a dynamic import to split this library off in a separate chunk + const csv = createCSVForGenomicBlast(jobResult.data); + + const zip = new JSZip(); + const rootFolder = zip.folder(getNameForZipRoot(submission)); + + const { sequenceId } = jobResult; + + const speciesFolderName = `${species.scientific_name}-${species.genome_id}`; + const sequenceFolderName = `Query sequence ${sequenceId}`; // NOTE: this name will probably change as well + const csvFileName = 'table'; + + rootFolder?.file( + `${speciesFolderName}/${sequenceFolderName}/${csvFileName}.csv`, + csv + ); + + const blob = await zip.generateAsync({ type: 'blob' }); + + await downloadBlobAsFile(blob, zipFileName); + }; + return (
{ theme="dark" className={styles.hitsTable} expandedContent={expandedContent} - disabledActions={[TableAction.FILTERS]} - exportFileName={`ensembl-blast_${jobId}.csv`} + disabledActions={[ + TableAction.FILTERS, + TableAction.FIND_IN_TABLE, + TableAction.DOWNLOAD_SHOWN_DATA + ]} + downloadHandler={downloadHandler} + downloadFileName={zipFileName} />
); diff --git a/src/shared/components/data-table/DataTable.tsx b/src/shared/components/data-table/DataTable.tsx index 7af28fce99..bf45e24dc3 100644 --- a/src/shared/components/data-table/DataTable.tsx +++ b/src/shared/components/data-table/DataTable.tsx @@ -40,7 +40,8 @@ type TableContextType = DataTableState & { selectableColumnIndex: number; expandedContent: { [rowId: string]: ReactNode }; disabledActions?: TableAction[]; - exportFileName?: string; + downloadFileName?: string; + downloadHandler?: () => void; rows: TableRows; }; @@ -56,7 +57,8 @@ export type TableProps = { expandedContent: { [rowId: string]: ReactNode }; disabledActions?: TableAction[]; className?: string; - exportFileName?: string; + downloadFileName?: string; + downloadHandler?: () => void; onStateChange?: (newState: DataTableState) => void; }; const DataTable = (props: TableProps) => { @@ -99,7 +101,8 @@ const DataTable = (props: TableProps) => { selectableColumnIndex: props.selectableColumnIndex, expandedContent: props.expandedContent, disabledActions: props.disabledActions, - exportFileName: props.exportFileName + downloadFileName: props.downloadFileName, + downloadHandler: props.downloadHandler }} >
diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/TableActions.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/TableActions.tsx index 1ee76a2faa..bb112a7327 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/TableActions.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/TableActions.tsx @@ -55,7 +55,7 @@ const actionOptions = [ }, { value: TableAction.DOWNLOAD_ALL_DATA, - label: 'Download all data' + label: 'Download this table' }, { value: TableAction.RESTORE_DEFAULTS, diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx index d10c88bc6e..611a8ea3d3 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -39,10 +39,11 @@ const DownloadData = () => { const { dispatch, rows, - exportFileName, + downloadFileName, columns, selectedAction, - hiddenRowIds + hiddenRowIds, + downloadHandler } = useDataTable(); const onCancel = () => { @@ -53,6 +54,10 @@ const DownloadData = () => { }; const handleDownload = () => { + if (downloadHandler) { + downloadHandler(); + return; + } const dataForExport: string[][] = []; dataForExport[0] = [ ...columns @@ -86,12 +91,12 @@ const DownloadData = () => { const csv = formatCSV(dataForExport); - downloadTextAsFile(csv, exportFileName ?? 'Table export.csv'); + downloadTextAsFile(csv, downloadFileName ?? 'Table export.csv'); }; return (
- {exportFileName ?? 'Table export.csv'} + {downloadFileName ?? 'Table export.csv'} Download cancel From eda8ee33dbc2778b2281d609e52b9bd139276c76 Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Tue, 4 Oct 2022 14:20:27 +0100 Subject: [PATCH 03/11] Import reorder --- .../table-actions/components/download-data/DownloadData.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx index 611a8ea3d3..a2c5756b60 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -15,13 +15,14 @@ */ import React, { ReactNode } from 'react'; -import { TableAction } from 'src/shared/components/data-table/dataTableTypes'; import useDataTable from 'src/shared/components/data-table/hooks/useDataTable'; +import { downloadTextAsFile } from 'src/shared/helpers/downloadAsFile'; import { PrimaryButton } from 'src/shared/components/button/Button'; +import { TableAction } from 'src/shared/components/data-table/dataTableTypes'; + import styles from './DownloadData.scss'; -import { downloadTextAsFile } from 'src/shared/helpers/downloadAsFile'; const getReactNodeText = (node: ReactNode): string => { if (['string', 'number'].includes(typeof node)) { From 3982811d05c563339ae7773d118a959054a1aa7e Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Tue, 4 Oct 2022 16:41:14 +0100 Subject: [PATCH 04/11] Download single file --- .../SingleBlastJobResult.tsx | 34 +++---------------- .../components/download-data/DownloadData.tsx | 2 +- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx index a2ad933f2c..63357c0123 100644 --- a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx +++ b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx @@ -25,8 +25,7 @@ import BlastHitsDiagram from 'src/content/app/tools/blast/components/blast-hits- import BlastSequenceAlignment from 'src/content/app/tools/blast/components/blast-sequence-alignment/BlastSequenceAlignment'; import { createCSVForGenomicBlast } from 'src/content/app/tools/blast/blast-download/createBlastCSVTable'; -import { getNameForZipRoot } from 'src/content/app/tools/blast/blast-download/submissionDownload'; -import { downloadBlobAsFile } from 'src/shared/helpers/downloadAsFile'; +import { downloadTextAsFile } from 'src/shared/helpers/downloadAsFile'; import type { BlastHit, @@ -193,11 +192,7 @@ const SingleBlastJobResult = (props: SingleBlastJobResultProps) => {
{isExpanded && ( - + )}
); @@ -206,16 +201,13 @@ const SingleBlastJobResult = (props: SingleBlastJobResultProps) => { type HitsTableProps = { jobResult: SingleBlastJobResultProps['jobResult']; submission: BlastSubmission; - species: Species; }; const HitsTable = (props: HitsTableProps) => { - const { jobResult, submission, species } = props; + const { jobResult, submission } = props; const blastDatabase = submission.submittedData.parameters .database as DatabaseType; - const zipFileName = getNameForZipRoot(submission) + '.zip'; - const [tableState, setTableState] = useState>({ rowsPerPage: 100, sortedColumn: { @@ -347,26 +339,9 @@ const HitsTable = (props: HitsTableProps) => { }; const downloadHandler = async () => { - const JSZip = await import('jszip').then((module) => module.default); // use a dynamic import to split this library off in a separate chunk const csv = createCSVForGenomicBlast(jobResult.data); - const zip = new JSZip(); - const rootFolder = zip.folder(getNameForZipRoot(submission)); - - const { sequenceId } = jobResult; - - const speciesFolderName = `${species.scientific_name}-${species.genome_id}`; - const sequenceFolderName = `Query sequence ${sequenceId}`; // NOTE: this name will probably change as well - const csvFileName = 'table'; - - rootFolder?.file( - `${speciesFolderName}/${sequenceFolderName}/${csvFileName}.csv`, - csv - ); - - const blob = await zip.generateAsync({ type: 'blob' }); - - await downloadBlobAsFile(blob, zipFileName); + await downloadTextAsFile(csv, 'table.csv'); }; return ( @@ -384,7 +359,6 @@ const HitsTable = (props: HitsTableProps) => { TableAction.DOWNLOAD_SHOWN_DATA ]} downloadHandler={downloadHandler} - downloadFileName={zipFileName} /> ); diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx index a2c5756b60..f982da3f50 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -97,7 +97,7 @@ const DownloadData = () => { return (
- {downloadFileName ?? 'Table export.csv'} + {downloadFileName ?? 'table.csv'} Download cancel From e3399209b7414f1b758401a46289fc27d573c986 Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Thu, 6 Oct 2022 12:03:26 +0100 Subject: [PATCH 05/11] Update getReactNodeText --- .../blast-download/submissionDownload.ts | 2 +- .../SingleBlastJobResult.tsx | 16 +++++++-- .../components/download-data/DownloadData.tsx | 35 +++++++++++++------ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/content/app/tools/blast/blast-download/submissionDownload.ts b/src/content/app/tools/blast/blast-download/submissionDownload.ts index f257115b63..5fa82af513 100644 --- a/src/content/app/tools/blast/blast-download/submissionDownload.ts +++ b/src/content/app/tools/blast/blast-download/submissionDownload.ts @@ -168,7 +168,7 @@ const createZipArchive = async (submission: EnrichedBlastSubmission) => { return zip; }; -export const getNameForZipRoot = (submission: BlastSubmission) => { +const getNameForZipRoot = (submission: BlastSubmission) => { const { id, submittedData: { diff --git a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx index 63357c0123..d904e90c41 100644 --- a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx +++ b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx @@ -24,7 +24,11 @@ import ShowHide from 'src/shared/components/show-hide/ShowHide'; import BlastHitsDiagram from 'src/content/app/tools/blast/components/blast-hits-diagram/BlastHitsDiagram'; import BlastSequenceAlignment from 'src/content/app/tools/blast/components/blast-sequence-alignment/BlastSequenceAlignment'; -import { createCSVForGenomicBlast } from 'src/content/app/tools/blast/blast-download/createBlastCSVTable'; +import { + createCSVForGenomicBlast, + createCSVForProteinBlast, + createCSVForTranscriptBlast +} from 'src/content/app/tools/blast/blast-download/createBlastCSVTable'; import { downloadTextAsFile } from 'src/shared/helpers/downloadAsFile'; import type { @@ -339,7 +343,14 @@ const HitsTable = (props: HitsTableProps) => { }; const downloadHandler = async () => { - const csv = createCSVForGenomicBlast(jobResult.data); + let csv = ''; + if (blastDatabase === 'dna') { + csv = createCSVForGenomicBlast(jobResult.data); + } else if (blastDatabase === 'cdna') { + csv = createCSVForTranscriptBlast(jobResult.data); + } else if (blastDatabase === 'pep') { + csv = createCSVForProteinBlast(jobResult.data); + } await downloadTextAsFile(csv, 'table.csv'); }; @@ -397,6 +408,7 @@ const getDynamicColumnContent = (props: DynamicColumnContentProps) => { return ( + hello {`${hit.hit_acc}:${[hitHsp.hsp_hit_from, hitHsp.hsp_hit_to] .sort() .join('-')}`} diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx index f982da3f50..be0066d618 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -14,26 +14,32 @@ * limitations under the License. */ import React, { ReactNode } from 'react'; +import ReactDOM from 'react-dom/client'; +import { memoize } from 'lodash'; import useDataTable from 'src/shared/components/data-table/hooks/useDataTable'; import { downloadTextAsFile } from 'src/shared/helpers/downloadAsFile'; -import { PrimaryButton } from 'src/shared/components/button/Button'; +import LoadingButton from 'src/shared/components/loading-button'; import { TableAction } from 'src/shared/components/data-table/dataTableTypes'; import styles from './DownloadData.scss'; -const getReactNodeText = (node: ReactNode): string => { - if (['string', 'number'].includes(typeof node)) { - return node?.toString() || ''; - } +const getReactRenderer = memoize(() => { + const element = document.createElement('div'); + const root = ReactDOM.createRoot(element); - if (typeof node === 'object' && node && 'props' in node) { - return getReactNodeText(node.props.children); - } + return { + element, + renderer: root + }; +}); - return ''; +const getReactNodeText = (node: ReactNode): string => { + const { element, renderer } = getReactRenderer(); + renderer.render(node); + return element.innerText; }; const DownloadData = () => { @@ -54,7 +60,7 @@ const DownloadData = () => { }); }; - const handleDownload = () => { + const handleDownload = async () => { if (downloadHandler) { downloadHandler(); return; @@ -95,10 +101,17 @@ const DownloadData = () => { downloadTextAsFile(csv, downloadFileName ?? 'Table export.csv'); }; + const onSuccess = () => { + // show the green tick momentarily before we close it + setTimeout(onCancel, 1000); + }; + return (
{downloadFileName ?? 'table.csv'} - Download + + Download + cancel From 79743d810cea4572d7c07fb3d569ed5dfc809cdf Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Thu, 6 Oct 2022 14:26:30 +0100 Subject: [PATCH 06/11] protein -> pep --- .../app/tools/blast/blast-download/submissionDownload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/app/tools/blast/blast-download/submissionDownload.ts b/src/content/app/tools/blast/blast-download/submissionDownload.ts index 5fa82af513..5397ea0122 100644 --- a/src/content/app/tools/blast/blast-download/submissionDownload.ts +++ b/src/content/app/tools/blast/blast-download/submissionDownload.ts @@ -100,7 +100,7 @@ const downloadBlastSubmission = async ( csv = createCSVForGenomicBlast(job.data); } else if (blastedAgainst === 'cdna') { csv = createCSVForTranscriptBlast(job.data); - } else if (blastedAgainst === 'protein') { + } else if (blastedAgainst === 'pep') { csv = createCSVForProteinBlast(job.data); } From 2e397c629bd112bb09b934511013293db38ca263 Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Fri, 7 Oct 2022 10:12:22 +0100 Subject: [PATCH 07/11] PR review cleanups --- .../SingleBlastJobResult.tsx | 1 - src/shared/components/data-table/DataTable.tsx | 4 ++-- .../components/download-data/DownloadData.tsx | 18 +++++++----------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx index d904e90c41..5e0502670a 100644 --- a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx +++ b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx @@ -408,7 +408,6 @@ const getDynamicColumnContent = (props: DynamicColumnContentProps) => { return ( - hello {`${hit.hit_acc}:${[hitHsp.hsp_hit_from, hitHsp.hsp_hit_to] .sort() .join('-')}`} diff --git a/src/shared/components/data-table/DataTable.tsx b/src/shared/components/data-table/DataTable.tsx index bf45e24dc3..3ea7572dd2 100644 --- a/src/shared/components/data-table/DataTable.tsx +++ b/src/shared/components/data-table/DataTable.tsx @@ -41,7 +41,7 @@ type TableContextType = DataTableState & { expandedContent: { [rowId: string]: ReactNode }; disabledActions?: TableAction[]; downloadFileName?: string; - downloadHandler?: () => void; + downloadHandler?: () => Promise; rows: TableRows; }; @@ -58,7 +58,7 @@ export type TableProps = { disabledActions?: TableAction[]; className?: string; downloadFileName?: string; - downloadHandler?: () => void; + downloadHandler?: () => Promise; onStateChange?: (newState: DataTableState) => void; }; const DataTable = (props: TableProps) => { diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx index be0066d618..d7354c06d6 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -15,7 +15,7 @@ */ import React, { ReactNode } from 'react'; import ReactDOM from 'react-dom/client'; -import { memoize } from 'lodash'; +import memoize from 'lodash/memoize'; import useDataTable from 'src/shared/components/data-table/hooks/useDataTable'; import { downloadTextAsFile } from 'src/shared/helpers/downloadAsFile'; @@ -53,7 +53,7 @@ const DownloadData = () => { downloadHandler } = useDataTable(); - const onCancel = () => { + const restoreDefaults = () => { dispatch({ type: 'set_selected_action', payload: TableAction.DEFAULT @@ -62,7 +62,8 @@ const DownloadData = () => { const handleDownload = async () => { if (downloadHandler) { - downloadHandler(); + await downloadHandler(); + setTimeout(restoreDefaults, 1000); return; } const dataForExport: string[][] = []; @@ -99,20 +100,15 @@ const DownloadData = () => { const csv = formatCSV(dataForExport); downloadTextAsFile(csv, downloadFileName ?? 'Table export.csv'); - }; - const onSuccess = () => { - // show the green tick momentarily before we close it - setTimeout(onCancel, 1000); + setTimeout(restoreDefaults, 1000); }; return (
{downloadFileName ?? 'table.csv'} - - Download - - + Download + cancel
From 47b88c23d00746e4e492235615140d1d3d216fe0 Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Tue, 11 Oct 2022 14:02:11 +0100 Subject: [PATCH 08/11] Update timeout logic --- .../components/download-data/DownloadData.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx index d7354c06d6..5d29a520fe 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useEffect, useRef } from 'react'; import ReactDOM from 'react-dom/client'; import memoize from 'lodash/memoize'; @@ -53,11 +53,23 @@ const DownloadData = () => { downloadHandler } = useDataTable(); + const allowComponentResetRef = useRef(true); + + useEffect(() => { + return () => { + allowComponentResetRef.current = false; + }; + }, []); + const restoreDefaults = () => { - dispatch({ - type: 'set_selected_action', - payload: TableAction.DEFAULT - }); + setTimeout(() => { + if (allowComponentResetRef.current) { + dispatch({ + type: 'set_selected_action', + payload: TableAction.DEFAULT + }); + } + }, 1000); }; const handleDownload = async () => { @@ -101,7 +113,7 @@ const DownloadData = () => { downloadTextAsFile(csv, downloadFileName ?? 'Table export.csv'); - setTimeout(restoreDefaults, 1000); + restoreDefaults(); }; return ( From 9df196831b3d7dfcf0c6bb2d85a74ae89bb59f3e Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Thu, 13 Oct 2022 13:14:18 +0100 Subject: [PATCH 09/11] Use controlled loading button --- .../components/download-data/DownloadData.tsx | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx index 5d29a520fe..93f14f9b33 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ReactNode, useEffect, useRef } from 'react'; +import React, { ReactNode, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom/client'; import memoize from 'lodash/memoize'; import useDataTable from 'src/shared/components/data-table/hooks/useDataTable'; import { downloadTextAsFile } from 'src/shared/helpers/downloadAsFile'; -import LoadingButton from 'src/shared/components/loading-button'; +import { ControlledLoadingButton } from 'src/shared/components/loading-button'; import { TableAction } from 'src/shared/components/data-table/dataTableTypes'; +import { LoadingState } from 'src/shared/types/loading-state'; import styles from './DownloadData.scss'; @@ -53,31 +54,42 @@ const DownloadData = () => { downloadHandler } = useDataTable(); + const [downloadState, setDownloadState] = useState( + LoadingState.NOT_REQUESTED + ); const allowComponentResetRef = useRef(true); useEffect(() => { + allowComponentResetRef.current = true; return () => { allowComponentResetRef.current = false; }; }, []); const restoreDefaults = () => { - setTimeout(() => { - if (allowComponentResetRef.current) { - dispatch({ - type: 'set_selected_action', - payload: TableAction.DEFAULT - }); - } - }, 1000); + if (allowComponentResetRef.current) { + dispatch({ + type: 'set_selected_action', + payload: TableAction.DEFAULT + }); + } }; const handleDownload = async () => { + setDownloadState(LoadingState.LOADING); if (downloadHandler) { - await downloadHandler(); - setTimeout(restoreDefaults, 1000); + try { + await downloadHandler(); + setDownloadState(LoadingState.SUCCESS); + setTimeout(restoreDefaults, 1000); + } catch { + setDownloadState(LoadingState.ERROR); + setTimeout(() => setDownloadState(LoadingState.NOT_REQUESTED), 2000); + } + return; } + const dataForExport: string[][] = []; dataForExport[0] = [ ...columns @@ -104,7 +116,13 @@ const DownloadData = () => { }) : cell; - dataForExport[rowIndex + 1].push(getReactNodeText(cellExportData)); + if (typeof cellExportData === 'string') { + dataForExport[rowIndex + 1].push(cellExportData); + } else if (typeof cellExportData === 'number') { + dataForExport[rowIndex + 1].push(String(cellExportData)); + } else { + dataForExport[rowIndex + 1].push(getReactNodeText(cellExportData)); + } } }); }); @@ -112,14 +130,16 @@ const DownloadData = () => { const csv = formatCSV(dataForExport); downloadTextAsFile(csv, downloadFileName ?? 'Table export.csv'); - - restoreDefaults(); + setDownloadState(LoadingState.SUCCESS); + setTimeout(restoreDefaults, 1000); }; return (
{downloadFileName ?? 'table.csv'} - Download + + Download + cancel From 26913addf2d32d8b087ce18174e4010e050d9c33 Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Thu, 13 Oct 2022 14:00:26 +0100 Subject: [PATCH 10/11] Use hsp_bit_score --- .../components/single-blast-job-result/SingleBlastJobResult.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx index 5e0502670a..2cb0c7aefe 100644 --- a/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx +++ b/src/content/app/tools/blast/views/blast-submission-results/components/single-blast-job-result/SingleBlastJobResult.tsx @@ -241,7 +241,7 @@ const HitsTable = (props: HitsTableProps) => { hitHsp.hsp_align_len, '', // view_alignment hitHsp.hsp_identity, - hitHsp.hsp_score, + hitHsp.hsp_bit_score, getDynamicColumnContent({ hit, blastDatabase, From a86f7f3621a81a9a59397dc20da116627ea9b531 Mon Sep 17 00:00:00 2001 From: Manoj Pandian Sakthivel Date: Thu, 13 Oct 2022 22:46:13 +0100 Subject: [PATCH 11/11] Check allowComponentResetRef inside setTimeout --- .../table-actions/components/download-data/DownloadData.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx index 93f14f9b33..3736d34952 100644 --- a/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx +++ b/src/shared/components/data-table/components/table-controls/components/table-actions/components/download-data/DownloadData.tsx @@ -84,7 +84,11 @@ const DownloadData = () => { setTimeout(restoreDefaults, 1000); } catch { setDownloadState(LoadingState.ERROR); - setTimeout(() => setDownloadState(LoadingState.NOT_REQUESTED), 2000); + setTimeout(() => { + if (allowComponentResetRef.current) { + setDownloadState(LoadingState.NOT_REQUESTED); + } + }, 2000); } return;