From 243ff74c2364b0324f76c69ea7eae54cd1dabcc1 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 09:53:52 +0200 Subject: [PATCH 01/20] f-4087/update: add download by clicking on the icon on tooltip f-4087/update: enhance key binding style --- .../ResourceEditor/ResourceEditor.less | 16 ++++++ .../ResourceEditor/useEditorTooltip.tsx | 54 +++++++++++++++---- src/shared/images/DownloadingLoop.svg | 2 +- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index ebfa1d05c..8a5fd8c74 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -205,6 +205,7 @@ height: 24px; margin-left: 10px; color: @fusion-primary-color; + cursor: pointer; } .key-binding { margin-left: 10px; @@ -220,3 +221,18 @@ gap: 20px; width: 100%; } + +kbd { + background-color: #eee; + border-radius: 3px; + border: 1px solid #b4b4b4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 2px 0 0 rgba(255, 255, 255, 0.7) inset; + color: #333; + display: inline-block; + font-size: 0.85em; + font-weight: 700; + line-height: 1; + padding: 2px 4px; + white-space: nowrap; +} diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index 693c019cd..30c2c2afd 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -19,7 +19,9 @@ const downloadImg = require('../../images/DownloadingLoop.svg'); type TTooltipCreator = Pick< TEditorPopoverResolvedData, 'error' | 'resolvedAs' | 'results' ->; +> & { + onDownload?: () => void; +}; function removePopoversFromDOM() { const popovers = document.querySelectorAll( @@ -39,10 +41,12 @@ function createTooltipNode({ tag, title, isDownloadable, + onDownload, }: { tag: string | null; title: string; isDownloadable?: boolean; + onDownload?: (ev: MouseEvent) => void; }) { const tooltipItemContent = document.createElement('div'); tooltipItemContent.className = 'CodeMirror-hover-tooltip-item'; @@ -59,20 +63,32 @@ function createTooltipNode({ const nodeDownload = document.createElement('img'); nodeDownload.setAttribute('src', downloadImg); nodeDownload.classList.add('download-icon'); - tooltipItemContent.appendChild(nodeDownload); + nodeDownload.onclick = onDownload ?? null; const keyBinding = document.createElement('span'); keyBinding.className = 'key-binding'; // the user has to click and press option key on mac or alt key on windows const userAgent = navigator.userAgent; const isMac = userAgent.indexOf('Mac') !== -1; - keyBinding.appendChild( - document.createTextNode(isMac ? '⌥ + Click' : 'Alt + Click') - ); + const kbdOption = document.createElement('kbd'); + kbdOption.innerText = isMac ? '⌥' : 'Alt'; + const plus = document.createElement('span'); + plus.innerText = ' + '; + const kbdClick = document.createElement('kbd'); + kbdClick.innerText = 'Click'; + keyBinding.appendChild(kbdOption); + keyBinding.appendChild(plus); + keyBinding.appendChild(kbdClick); + tooltipItemContent.appendChild(nodeDownload); tooltipItemContent.appendChild(keyBinding); } return tooltipItemContent; } -function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { +function createTooltipContent({ + resolvedAs, + error, + results, + onDownload, +}: TTooltipCreator) { const tooltipContent = document.createElement('div'); tooltipContent.className = clsx( `${CODEMIRROR_HOVER_CLASS}-content`, @@ -91,6 +107,7 @@ function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { const result = results as TDELink; tooltipContent.appendChild( createTooltipNode({ + onDownload, tag: result.resource ? `${result.resource?.[0]}/${result.resource?.[1]}` : null, @@ -161,6 +178,7 @@ function useEditorTooltip({ projectLabel: string; }) { const nexus = useNexusContext(); + const { downloadBinaryAsyncHandler } = useResolutionActions(); const { config: { apiEndpoint }, } = useSelector((state: RootState) => ({ @@ -203,14 +221,12 @@ function useEditorTooltip({ hideTooltip(tooltip); tooltip.remove(); } - node.removeEventListener('mouseout', cleanup); node.removeEventListener('click', cleanup); - node.removeEventListener('scroll', cleanup); + editorWrapper.removeEventListener('scroll', cleanup); } - node.addEventListener('mouseout', cleanup); node.addEventListener('click', cleanup); - node.addEventListener('scroll', cleanup); + editorWrapper.addEventListener('scroll', cleanup); const timeoutId: ReturnType = setTimeout(() => { if (tooltip) { @@ -218,7 +234,7 @@ function useEditorTooltip({ tooltip.remove(); } return clearTimeout(timeoutId); - }, 2000); + }, 3000); return tooltip; } @@ -241,6 +257,22 @@ function useEditorTooltip({ resolvedAs, error, results, + onDownload: + resolvedAs === 'resource' && (results as TDELink).isDownloadable + ? () => { + const result = results as TDELink; + if (result.isDownloadable) { + return downloadBinaryAsyncHandler({ + orgLabel: result.resource?.[0]!, + projectLabel: result.resource?.[1]!, + resourceId: result.resource?.[2]!, + ext: result.resource?.[4] ?? 'json', + title: result.title, + }); + } + return; + } + : undefined, }); if (tooltipContent) { node.classList.remove('wait-for-tooltip'); diff --git a/src/shared/images/DownloadingLoop.svg b/src/shared/images/DownloadingLoop.svg index c1a71f086..5da0751ce 100644 --- a/src/shared/images/DownloadingLoop.svg +++ b/src/shared/images/DownloadingLoop.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 3ce00ca997515fe4d3e6e3dd852030b5043e7408 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 11:09:14 +0200 Subject: [PATCH 02/20] f-4085/update: add warning message when resolver api failed --- .../ResourceEditor/ResourceEditor.less | 31 ++++++++++++++++ .../ResourceEditor/useEditorTooltip.tsx | 35 +++++++++++++++++++ src/shared/images/InfoCircleLine.svg | 1 + 3 files changed, 67 insertions(+) create mode 100644 src/shared/images/InfoCircleLine.svg diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 8a5fd8c74..03b7b9787 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -207,6 +207,7 @@ color: @fusion-primary-color; cursor: pointer; } + .key-binding { margin-left: 10px; color: @fusion-primary-color; @@ -236,3 +237,33 @@ kbd { padding: 2px 4px; white-space: nowrap; } + +.CodeMirror-hover-tooltip-warning { + .warning-text { + background-color: rgba(red, 0.12); + margin-top: -4px; + padding: 5px 10px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + font-weight: 600; + } + + .warning-info { + padding: 4px 10px; + font-size: 13px; + line-height: 18px; + color: #0974ca; + display: flex; + align-items: flex-start; + justify-content: flex-start; + gap: 5px; + border-bottom: 1px solid #f0efef; + margin-bottom: 5px; + .warning-info-icon { + margin-top: 3px; + width: 16px; + height: 16px; + object-fit: cover; + } + } +} diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index 30c2c2afd..7cc28bf02 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -3,6 +3,7 @@ import CodeMirror from 'codemirror'; import clsx from 'clsx'; import { useNexusContext } from '@bbp/react-nexus'; import { useSelector } from 'react-redux'; +import * as pluralize from 'pluralize'; import { CODEMIRROR_HOVER_CLASS, TEditorPopoverResolvedData, @@ -15,6 +16,7 @@ import { RootState } from '../../store/reducers'; import useResolutionActions from './useResolutionActions'; const downloadImg = require('../../images/DownloadingLoop.svg'); +const infoImg = require('../../images/InfoCircleLine.svg'); type TTooltipCreator = Pick< TEditorPopoverResolvedData, @@ -83,6 +85,35 @@ function createTooltipNode({ } return tooltipItemContent; } + +function createWarningHeader(count: number) { + const warningHeader = document.createElement('div'); + warningHeader.className = 'CodeMirror-hover-tooltip-warning'; + const warningText = document.createElement('div'); + warningText.className = 'warning-text'; + warningText.appendChild( + document.createTextNode( + `We could not resolve this ID to an existing resource as configured in your project, you might need to create this resource or update the resolver configuration of this project.` + ) + ); + const warningInfo = document.createElement('div'); + warningInfo.className = 'warning-info'; + const warningInfoIcon = document.createElement('img'); + warningInfoIcon.className = 'warning-info-icon'; + warningInfoIcon.setAttribute('src', infoImg); + warningInfo.appendChild(warningInfoIcon); + warningInfo.appendChild( + document.createTextNode( + `For your information, searching across all projects where you have read access, we found the following matching ${pluralize( + 'resource', + count + )}:` + ) + ); + warningHeader.appendChild(warningText); + warningHeader.appendChild(warningInfo); + return warningHeader; +} function createTooltipContent({ resolvedAs, error, @@ -105,6 +136,8 @@ function createTooltipContent({ } if (resolvedAs === 'resource') { const result = results as TDELink; + const warningHeader = createWarningHeader(1); + tooltipContent.appendChild(warningHeader); tooltipContent.appendChild( createTooltipNode({ onDownload, @@ -118,6 +151,8 @@ function createTooltipContent({ return tooltipContent; } if (resolvedAs === 'resources') { + const warningHeader = createWarningHeader((results as TDELink[]).length); + tooltipContent.appendChild(warningHeader); tooltipContent.appendChild( createTooltipNode({ tag: 'Multiple', diff --git a/src/shared/images/InfoCircleLine.svg b/src/shared/images/InfoCircleLine.svg new file mode 100644 index 000000000..33d52b777 --- /dev/null +++ b/src/shared/images/InfoCircleLine.svg @@ -0,0 +1 @@ + \ No newline at end of file From 7a6685fc0d220855b585b25d19d1dd071b3b7ca3 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 11:14:17 +0200 Subject: [PATCH 03/20] f-4091/update: show all the control panel for the resource editor --- .../DataExplorerGraphFlowContent.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent.tsx b/src/shared/organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent.tsx index 8978494c6..6a8c8e9b1 100644 --- a/src/shared/organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent.tsx @@ -25,11 +25,11 @@ const DataExplorerContentPage = ({}) => { rev={current?.resource?.[3]!} defaultEditable={false} defaultExpanded={false} - showMetadataToggle={false} - showFullScreen={false} tabChange={false} - showExpanded={false} - showControlPanel={false} + showFullScreen={false} + showMetadataToggle={true} + showExpanded={true} + showControlPanel={true} /> ) : ( From 86af1daa7b4e70b8f1a468980d96a498a11ee7e5 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 11:26:39 +0200 Subject: [PATCH 04/20] f-4092/update: add toggle to fetch deprecated resources, default to false --- src/subapps/dataExplorer/DataExplorer.tsx | 26 ++++++++++++++++++- .../dataExplorer/DataExplorerUtils.tsx | 8 +++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index dda2c2a9d..498622f3c 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -23,6 +23,7 @@ export interface DataExplorerConfiguration { type: string | undefined; predicate: ((resource: Resource) => boolean) | null; selectedPath: string | null; + deprecated: boolean; } export const DataExplorer: React.FC<{}> = () => { @@ -31,7 +32,15 @@ export const DataExplorer: React.FC<{}> = () => { const [headerHeight, setHeaderHeight] = useState(0); const [ - { pageSize, offset, orgAndProject, predicate, type, selectedPath }, + { + pageSize, + offset, + orgAndProject, + predicate, + type, + selectedPath, + deprecated, + }, updateTableConfiguration, ] = useReducer( ( @@ -45,6 +54,7 @@ export const DataExplorer: React.FC<{}> = () => { type: undefined, predicate: null, selectedPath: null, + deprecated: false, } ); @@ -53,6 +63,7 @@ export const DataExplorer: React.FC<{}> = () => { offset, orgAndProject, type, + deprecated, }); const currentPageDataSource: Resource[] = resources?._results || []; @@ -71,6 +82,11 @@ export const DataExplorer: React.FC<{}> = () => { [currentPageDataSource, showMetadataColumns, selectedPath] ); + const onDeprecatedChange = (checked: boolean) => + updateTableConfiguration({ + deprecated: checked, + }); + return (
{isLoading && } @@ -111,6 +127,14 @@ export const DataExplorer: React.FC<{}> = () => { totalFiltered={predicate ? displayedDataSource.length : undefined} />
+ + { const nexus = useNexusContext(); return useQuery({ - queryKey: ['data-explorer', { pageSize, offset, orgAndProject, type }], + queryKey: [ + 'data-explorer', + { pageSize, offset, orgAndProject, type, deprecated }, + ], retry: false, queryFn: async () => { const resultWithPartialResources = await nexus.Resource.list( @@ -23,6 +27,7 @@ export const usePaginatedExpandedResources = ({ orgAndProject?.[1], { type, + deprecated, from: offset, size: pageSize, } @@ -181,4 +186,5 @@ interface PaginatedResourcesParams { offset: number; orgAndProject?: string[]; type?: string; + deprecated: boolean; } From c695d4aee85d896f692f9aa594daeec7b19eb46f Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 12:42:26 +0200 Subject: [PATCH 05/20] f-4094/update: add data cart logic to allow the user to download resources --- .../dataExplorer/DataExplorerTable.tsx | 188 +++++++++++++++++- src/subapps/dataExplorer/styles.less | 1 + 2 files changed, 181 insertions(+), 8 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index f742c1f7e..16077ceb2 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -1,17 +1,36 @@ +import React, { useEffect, useReducer } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; import { Resource } from '@bbp/nexus-sdk'; import { Empty, Table, Tooltip } from 'antd'; import { ColumnType, TablePaginationConfig } from 'antd/lib/table'; -import { isArray, isString, startCase } from 'lodash'; -import React from 'react'; -import { makeOrgProjectTuple } from '../../shared/molecules/MyDataTable/MyDataTable'; +import { isArray, isNil, isString, startCase } from 'lodash'; +import { SelectionSelectFn } from 'antd/lib/table/interface'; +import { clsx } from 'clsx'; + +import { + MAX_DATA_SELECTED_SIZE__IN_BYTES, + MAX_LOCAL_STORAGE_ALLOWED_SIZE, + TDataSource, + TResourceTableData, + getLocalStorageSize, + makeOrgProjectTuple, + notifyTotalSizeExeeced, +} from '../../shared/molecules/MyDataTable/MyDataTable'; import isValidUrl from '../../utils/validUrl'; import { NoDataCell } from './NoDataCell'; -import './styles.less'; import { DataExplorerConfiguration } from './DataExplorer'; -import { useHistory, useLocation } from 'react-router-dom'; import { makeResourceUri, parseProjectUrl } from '../../shared/utils'; -import { clsx } from 'clsx'; import { FUSION_TITLEBAR_HEIGHT } from './DataExplorerCollapsibleHeader'; +import { + DATA_PANEL_STORAGE, + DATA_PANEL_STORAGE_EVENT, + DataPanelEvent, +} from '../../shared/organisms/DataPanel/DataPanel'; +import { + removeLocalStorageRows, + toLocalStorageResources, +} from '../../shared/utils/datapanel'; +import './styles.less'; interface TDataExplorerTable { isLoading: boolean; @@ -25,8 +44,13 @@ interface TDataExplorerTable { tableOffsetFromTop: number; } -type TColumnNameToConfig = Map>; +type TDateExplorerTableData = { + selectedRowKeys: React.Key[]; + selectedRows: TDataSource[]; +}; +type TColumnNameToConfig = Map>; +const DATA_EXPLORER_NAMESPACE = 'data-explorer'; export const DataExplorerTable: React.FC = ({ isLoading, dataSource, @@ -40,7 +64,19 @@ export const DataExplorerTable: React.FC = ({ }: TDataExplorerTable) => { const history = useHistory(); const location = useLocation(); - + const [{ selectedRowKeys }, updateTableData] = useReducer( + ( + previous: TResourceTableData, + partialData: Partial + ) => ({ + ...previous, + ...partialData, + }), + { + selectedRowKeys: [], + selectedRows: [], + } + ); const allowedTotal = total ? (total > 10000 ? 10000 : total) : undefined; const tablePaginationConfig: TablePaginationConfig = { @@ -69,6 +105,137 @@ export const DataExplorerTable: React.FC = ({ }); }; + const onSelectRowChange: SelectionSelectFn = async ( + record, + selected + ) => { + const recordKey = record._self; + const dataPanelLS: TDateExplorerTableData = JSON.parse( + localStorage.getItem(DATA_PANEL_STORAGE)! + ); + + const localStorageRows = toLocalStorageResources( + record, + DATA_EXPLORER_NAMESPACE + ); + let selectedRowKeys = dataPanelLS?.selectedRowKeys || []; + let selectedRows = dataPanelLS?.selectedRows || []; + + if (selected) { + selectedRowKeys = [...selectedRowKeys, recordKey]; + selectedRows = [...selectedRows, ...localStorageRows]; + } else { + selectedRowKeys = selectedRowKeys.filter(t => t !== recordKey); + selectedRows = removeLocalStorageRows(selectedRows, [recordKey]); + } + + const size = selectedRows.reduce( + (acc, item) => acc + (item.distribution?.contentSize || 0), + 0 + ); + if ( + size > MAX_DATA_SELECTED_SIZE__IN_BYTES || + getLocalStorageSize() > MAX_LOCAL_STORAGE_ALLOWED_SIZE + ) { + return notifyTotalSizeExeeced(); + } + localStorage.setItem( + DATA_PANEL_STORAGE, + JSON.stringify({ + selectedRowKeys, + selectedRows, + }) + ); + window.dispatchEvent( + new CustomEvent(DATA_PANEL_STORAGE_EVENT, { + detail: { + datapanel: { selectedRowKeys, selectedRows }, + }, + }) + ); + }; + const onSelectAllChange = async ( + selected: boolean, + tSelectedRows: Resource[], + changeRows: Resource[] + ) => { + const dataPanelLS: TDateExplorerTableData = JSON.parse( + localStorage.getItem(DATA_PANEL_STORAGE)! + ); + let selectedRowKeys = dataPanelLS?.selectedRowKeys || []; + let selectedRows = dataPanelLS?.selectedRows || []; + + if (selected) { + const results = changeRows.map(row => + toLocalStorageResources(row, DATA_EXPLORER_NAMESPACE) + ); + selectedRows = [...selectedRows, ...results.flat()]; + selectedRowKeys = [...selectedRowKeys, ...changeRows.map(t => t._self)]; + } else { + const rowKeysToRemove = changeRows.map(r => r._self); + + selectedRowKeys = selectedRowKeys.filter( + key => !rowKeysToRemove.includes(key.toString()) + ); + selectedRows = removeLocalStorageRows(selectedRows, rowKeysToRemove); + } + const size = selectedRows.reduce( + (acc, item) => acc + (item.distribution?.contentSize || 0), + 0 + ); + if ( + size > MAX_DATA_SELECTED_SIZE__IN_BYTES || + getLocalStorageSize() > MAX_LOCAL_STORAGE_ALLOWED_SIZE + ) { + return notifyTotalSizeExeeced(); + } + localStorage.setItem( + DATA_PANEL_STORAGE, + JSON.stringify({ + selectedRowKeys, + selectedRows, + }) + ); + window.dispatchEvent( + new CustomEvent(DATA_PANEL_STORAGE_EVENT, { + detail: { + datapanel: { selectedRowKeys, selectedRows }, + }, + }) + ); + }; + + useEffect(() => { + const dataLs = localStorage.getItem(DATA_PANEL_STORAGE); + const dataLsObject: TResourceTableData = JSON.parse(dataLs as string); + if (dataLs && dataLs.length) { + updateTableData({ + selectedRows: dataLsObject.selectedRows, + selectedRowKeys: dataLsObject.selectedRowKeys, + }); + } + }, []); + useEffect(() => { + const dataPanelEventListner = ( + event: DataPanelEvent<{ datapanel: TResourceTableData }> + ) => { + updateTableData({ + selectedRows: event.detail?.datapanel.selectedRows, + selectedRowKeys: event.detail?.datapanel.selectedRowKeys, + }); + }; + window.addEventListener( + DATA_PANEL_STORAGE_EVENT, + dataPanelEventListner as EventListener + ); + return () => { + window.removeEventListener( + DATA_PANEL_STORAGE_EVENT, + dataPanelEventListner as EventListener + ); + }; + }, []); + return (
= ({ return isLoading ? <> : ; }, }} + rowSelection={{ + selectedRowKeys, + onSelect: onSelectRowChange, + onSelectAll: onSelectAllChange, + }} pagination={tablePaginationConfig} sticky={{ offsetHeader: tableOffsetFromTop }} /> diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 7ef14836f..4feecda78 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -219,6 +219,7 @@ } .data-explorer-table { + margin-bottom: 60px; table { width: auto; min-width: 90vw !important; // This is needed to make sure that the table headers and table body columns are aligned when the table is rerendered (eg when user selects a predicate, project, or, type). From d345fac2164c3028666fba80d8a7861d8e5bf7f1 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 14:34:12 +0200 Subject: [PATCH 06/20] f-4084/fix: truncated headers of the columns --- src/subapps/dataExplorer/styles.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 7ef14836f..8bfdc133b 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -258,6 +258,9 @@ .ant-table-tbody > tr.data-explorer-row > td { border-bottom: 1px solid #d9d9d9; } + .ant-table-column-sorters { + justify-content: center !important; + } } .search-menu { From b94fe2a0c31ebb644a92ea154008bd884aa77883 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 15:11:39 +0200 Subject: [PATCH 07/20] f-4088/update: make the navigation items clickable on all places not just the icon --- .../NavigationStackItem.tsx | 18 +----------------- .../DataExplorerGraphFlowMolecules/styles.less | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx index 47e5e10a2..aa94aa956 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -99,23 +99,7 @@ const NavigationStackItem = ({ } > {collapseRightBtn()} -
- - {orgProject && {orgProject}} - {decodeURIComponent(_self)} -
- } - > - - +
{orgProject && {orgProject}} {title &&
{title}
} {types && ( diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index b3a2b5346..c4bac218e 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -16,11 +16,18 @@ justify-items: center; justify-content: center; align-content: start; - padding: 3px 5px; + padding: 10px 5px; height: 100%; min-height: 0; background-color: @fusion-main-bg; position: relative; + user-select: none; + cursor: pointer; + &:hover { + background-color: white; + color: @fusion-primary-color; + box-shadow: 0 2px 12px rgba(#333, 0.12); + } &.more { background-color: white; @@ -53,7 +60,7 @@ .org-project { writing-mode: vertical-rl; transform: rotate(-180deg); - background-color: #bfbfbfbe; + background-color: #bfbfbf7c; padding: 5px 1px; font-size: 10px; border-radius: 4px; @@ -129,6 +136,7 @@ &.right { border-top-left-radius: 4px; border-bottom-left-radius: 4px; + svg { transform: rotate(180deg); } @@ -166,21 +174,26 @@ gap: 5px; z-index: 90; border: none; + &:hover { text-shadow: 0 2px 12px rgba(#333, 0.12); + span { color: #377af5; } + svg { transform: scale(1.1); transition: transform 0.2s ease-in-out; } } + span { font-weight: 700; font-size: 16px; color: @fusion-daybreak-8; } + &:disabled { cursor: not-allowed; opacity: 0.5; From 5ab9d9862d13ec5fd1cba4c5dfd4df8ba5499894 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 15:52:29 +0200 Subject: [PATCH 08/20] f-4088/fix: update the tests --- .../NavigationStack.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index 044951630..b2bfde841 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -379,7 +379,7 @@ describe('NavigationStack', () => { rerender(app); // select by class and role of open-naivation-item const forthNodeNavigationItem = container.querySelector( - '.navigation-stack-item.left.item-4 .navigation-stack-item__wrapper > .icon[role="open-navigation-item"]' + '.navigation-stack-item.left.item-4 .navigation-stack-item__wrapper' ); expect(forthNodeNavigationItem).not.toBeNull(); expect(forthNodeNavigationItem).toBeInTheDocument(); From 5a64792521ba6e8bd8dad3577c5e85d4479fa2ba Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 16:35:09 +0200 Subject: [PATCH 09/20] f-4090/update: results summary re-phrase --- .../molecules/MyDataTable/MyDataTable.tsx | 2 +- src/subapps/dataExplorer/DataExplorer.tsx | 2 + src/subapps/dataExplorer/DatasetCount.tsx | 38 ++++++++++++++----- src/subapps/dataExplorer/styles.less | 2 +- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/shared/molecules/MyDataTable/MyDataTable.tsx b/src/shared/molecules/MyDataTable/MyDataTable.tsx index 82cf6a1dd..1d5ebaa80 100644 --- a/src/shared/molecules/MyDataTable/MyDataTable.tsx +++ b/src/shared/molecules/MyDataTable/MyDataTable.tsx @@ -132,7 +132,7 @@ export const notifyTotalSizeExeeced = () => { key: 'data-panel-size-exceeded', }); }; -const getTypesTrancated = (text: string | string[]) => { +export const getTypesTrancated = (text: string | string[]) => { let types = ''; let typesWithUrl = text; if (isArray(text)) { diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index dda2c2a9d..0a93ae1a9 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -106,6 +106,8 @@ export const DataExplorer: React.FC<{}> = () => {
= ({ nexusTotal, totalOnPage, totalFiltered, + orgAndProject, + type, }: Props) => { return (
- + Total:{' '} {nexusTotal.toLocaleString(`en-US`)}{' '} {pluralize('dataset', nexusTotal ?? 0)} {' '} + {orgAndProject && orgAndProject.length === 2 && ( + + in project{' '} + + {orgAndProject[0]}/{orgAndProject[1]} + + + )}{' '} + {type && ( + + of type {getTypesTrancated(type).types} + + )} - - - Sample loaded for review: {totalOnPage} - - - {!isNil(totalFiltered) && ( + - Filtered: {totalFiltered} - - )} + Sample loaded for review: {totalOnPage} + {' '} + {!isNil(totalFiltered) && ( + + of which {totalFiltered} matching filter + + )} +
); }; diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 7ef14836f..1170125fb 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -54,7 +54,7 @@ color: @fusion-neutral-7; margin-left: 20px; - span { + .total { margin-right: 24px; } } From 5c43160f9b706b43ddca755f09338abe52b3283f Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 17:13:12 +0200 Subject: [PATCH 10/20] f-4090/update: result summary test --- src/subapps/dataExplorer/DataExplorer.spec.tsx | 7 +------ src/subapps/dataExplorer/DatasetCount.tsx | 6 ++---- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 7b72f386b..c1b22829c 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -317,7 +317,7 @@ describe('DataExplorer', () => { }; const getFilteredResultsCount = async (expectedCount: number = 0) => { - const filteredCountLabel = await screen.queryByText('Filtered:'); + const filteredCountLabel = screen.queryByText(/of which/i); if (!filteredCountLabel) { return filteredCountLabel; } @@ -592,7 +592,6 @@ describe('DataExplorer', () => { it('shows resources filtered by the selected project', async () => { await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); - visibleTableRows().forEach(row => expect(projectFromRow(row)).toMatch(/unhcr/i) ); @@ -808,9 +807,6 @@ describe('DataExplorer', () => { it('shows total filtered count if predicate is selected', async () => { await expectRowCountToBe(10); - const totalFromFrontendBefore = await getFilteredResultsCount(); - expect(totalFromFrontendBefore).toEqual(null); - await updateResourcesShownInTable([ getMockResource('self1', { author: 'piggy', edition: 1 }), getMockResource('self2', { author: ['iggy', 'twinky'] }), @@ -821,7 +817,6 @@ describe('DataExplorer', () => { await userEvent.click(container); await selectOptionFromMenu(PredicateMenuLabel, EXISTS); await expectRowCountToBe(2); - const totalFromFrontendAfter = await getFilteredResultsCount(2); expect(totalFromFrontendAfter).toBeVisible(); }); diff --git a/src/subapps/dataExplorer/DatasetCount.tsx b/src/subapps/dataExplorer/DatasetCount.tsx index 534a23de5..89840d03f 100644 --- a/src/subapps/dataExplorer/DatasetCount.tsx +++ b/src/subapps/dataExplorer/DatasetCount.tsx @@ -42,11 +42,9 @@ export const DatasetCount: React.FC = ({ )} - - Sample loaded for review: {totalOnPage} - {' '} + Sample loaded for review: {totalOnPage}{' '} {!isNil(totalFiltered) && ( - + of which {totalFiltered} matching filter )} From cd5661063d1359dbabd183d9a9effde1d0086e95 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 25 Jul 2023 17:13:22 +0200 Subject: [PATCH 11/20] f-4089/update: style of fullscreen toggle --- src/shared/App.less | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/shared/App.less b/src/shared/App.less index aeafdfde5..a00505448 100644 --- a/src/shared/App.less +++ b/src/shared/App.less @@ -236,22 +236,34 @@ align-items: center; justify-content: center; gap: 10px; + span { color: @fusion-blue-8; } + .full-screen-switch { - border-color: #2e76bf !important; - background: linear-gradient( - 0deg, - rgba(0, 58, 140, 0.3), - rgba(0, 58, 140, 0.3) - ), + border: 1px solid #2e76bf !important; + background: white; + + .ant-switch-handle { + top: 1px; + + &::before { + background: @fusion-blue-8; + } + } + } + + .full-screen-switch.ant-switch-checked { + border: 1px solid #2e76bf !important; + background: linear-gradient(0deg, @fusion-blue-8, @fusion-blue-8), linear-gradient(0deg, rgba(46, 118, 191, 0.2), rgba(46, 118, 191, 0.2)); - border: 1px solid #003a8c4d; + .ant-switch-handle { top: 1px; + &::before { - background: @fusion-daybreak-10; + background: white; } } } From ab62cd91e1974c29870040d624401ce346d65fed Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 26 Jul 2023 15:31:53 +0200 Subject: [PATCH 12/20] f-4086/update: add scroll arrows for data explorer --- .../components/Icons/LeftSpeedArrow.tsx | 36 ++++ .../components/Icons/RightSpeedArrow.tsx | 37 ++++ src/subapps/dataExplorer/DataExplorer.tsx | 19 +- .../dataExplorer/DataExplorerTable.tsx | 170 ++++++++-------- .../dataExplorer/DateExplorerScrollArrows.tsx | 188 ++++++++++++++++++ src/subapps/dataExplorer/styles.less | 80 ++++++++ 6 files changed, 445 insertions(+), 85 deletions(-) create mode 100644 src/shared/components/Icons/LeftSpeedArrow.tsx create mode 100644 src/shared/components/Icons/RightSpeedArrow.tsx create mode 100644 src/subapps/dataExplorer/DateExplorerScrollArrows.tsx diff --git a/src/shared/components/Icons/LeftSpeedArrow.tsx b/src/shared/components/Icons/LeftSpeedArrow.tsx new file mode 100644 index 000000000..bd5078af0 --- /dev/null +++ b/src/shared/components/Icons/LeftSpeedArrow.tsx @@ -0,0 +1,36 @@ +import { SVGProps } from 'react'; + +const LeftSpeedArrow = (props: SVGProps) => { + return ( + + + + ); +}; +const LeftArrow = (props: SVGProps) => { + return ( + + + + ); +}; +export { LeftArrow }; +export default LeftSpeedArrow; diff --git a/src/shared/components/Icons/RightSpeedArrow.tsx b/src/shared/components/Icons/RightSpeedArrow.tsx new file mode 100644 index 000000000..9efb8c9fd --- /dev/null +++ b/src/shared/components/Icons/RightSpeedArrow.tsx @@ -0,0 +1,37 @@ +import { SVGProps } from 'react'; + +const RightSpeedArrow = (props: SVGProps) => { + return ( + + + + ); +}; + +const RightArrow = (props: SVGProps) => { + return ( + + + + ); +}; +export { RightArrow }; +export default RightSpeedArrow; diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index dda2c2a9d..639c784fa 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -1,6 +1,6 @@ import { Resource } from '@bbp/nexus-sdk'; import { Spin, Switch } from 'antd'; -import React, { useMemo, useReducer, useState } from 'react'; +import React, { useMemo, useReducer, useRef, useState } from 'react'; import { DataExplorerTable } from './DataExplorerTable'; import { columnFromPath, @@ -15,6 +15,7 @@ import { TypeSelector } from './TypeSelector'; import './styles.less'; import { DataExplorerCollapsibleHeader } from './DataExplorerCollapsibleHeader'; import Loading from '../../shared/components/Loading'; +import DateExplorerScrollArrows from './DateExplorerScrollArrows'; export interface DataExplorerConfiguration { pageSize: number; @@ -70,9 +71,11 @@ export const DataExplorer: React.FC<{}> = () => { ), [currentPageDataSource, showMetadataColumns, selectedPath] ); - + const containerRef = useRef(null); + const tableRef = useRef(null); + console.log('DataExplorer', tableRef.current); return ( -
+
{isLoading && } = () => {
= () => { showEmptyDataCells={showEmptyDataCells} tableOffsetFromTop={headerHeight} /> +
); }; diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index f742c1f7e..91264792d 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -2,7 +2,7 @@ import { Resource } from '@bbp/nexus-sdk'; import { Empty, Table, Tooltip } from 'antd'; import { ColumnType, TablePaginationConfig } from 'antd/lib/table'; import { isArray, isString, startCase } from 'lodash'; -import React from 'react'; +import React, { forwardRef } from 'react'; import { makeOrgProjectTuple } from '../../shared/molecules/MyDataTable/MyDataTable'; import isValidUrl from '../../utils/validUrl'; import { NoDataCell } from './NoDataCell'; @@ -27,89 +27,95 @@ interface TDataExplorerTable { type TColumnNameToConfig = Map>; -export const DataExplorerTable: React.FC = ({ - isLoading, - dataSource, - columns, - total, - pageSize, - offset, - updateTableConfiguration, - showEmptyDataCells, - tableOffsetFromTop, -}: TDataExplorerTable) => { - const history = useHistory(); - const location = useLocation(); - - const allowedTotal = total ? (total > 10000 ? 10000 : total) : undefined; - - const tablePaginationConfig: TablePaginationConfig = { - pageSize, - total: allowedTotal, - pageSizeOptions: [10, 20, 50], - position: ['bottomLeft'], - defaultPageSize: 50, - defaultCurrent: 0, - current: offset / pageSize + 1, - onChange: (page, _) => - updateTableConfiguration({ offset: (page - 1) * pageSize }), - onShowSizeChange: (_, size) => { - updateTableConfiguration({ pageSize: size, offset: 0 }); +export const DataExplorerTable = forwardRef( + ( + { + isLoading, + dataSource, + columns, + total, + pageSize, + offset, + updateTableConfiguration, + showEmptyDataCells, + tableOffsetFromTop, }, - showQuickJumper: true, - showSizeChanger: true, - }; - - const goToResource = (resource: Resource) => { - const resourceId = resource['@id'] ?? resource._self; - const [orgLabel, projectLabel] = parseProjectUrl(resource._project); - - history.push(makeResourceUri(orgLabel, projectLabel, resourceId), { - background: location, - }); - }; - - return ( -
- - columns={columnsConfig(columns, showEmptyDataCells)} - dataSource={dataSource} - rowKey={record => record._self} - onRow={resource => ({ - onClick: _ => goToResource(resource), - 'data-testid': resource._self, - })} - loading={{ spinning: isLoading, indicator: <> }} - bordered={false} - className={clsx( - 'data-explorer-table', - tableOffsetFromTop === FUSION_TITLEBAR_HEIGHT && - 'data-explorer-header-collapsed' - )} - rowClassName="data-explorer-row" - scroll={{ x: 'max-content' }} - locale={{ - emptyText() { - return isLoading ? <> : ; - }, + ref + ) => { + const history = useHistory(); + const location = useLocation(); + + const allowedTotal = total ? (total > 10000 ? 10000 : total) : undefined; + + const tablePaginationConfig: TablePaginationConfig = { + pageSize, + total: allowedTotal, + pageSizeOptions: [10, 20, 50], + position: ['bottomLeft'], + defaultPageSize: 50, + defaultCurrent: 0, + current: offset / pageSize + 1, + onChange: (page, _) => + updateTableConfiguration({ offset: (page - 1) * pageSize }), + onShowSizeChange: (_, size) => { + updateTableConfiguration({ pageSize: size, offset: 0 }); + }, + showQuickJumper: true, + showSizeChanger: true, + }; + + const goToResource = (resource: Resource) => { + const resourceId = resource['@id'] ?? resource._self; + const [orgLabel, projectLabel] = parseProjectUrl(resource._project); + + history.push(makeResourceUri(orgLabel, projectLabel, resourceId), { + background: location, + }); + }; + + return ( +
-
- ); -}; + > + + columns={columnsConfig(columns, showEmptyDataCells)} + dataSource={dataSource} + rowKey={record => record._self} + onRow={resource => ({ + onClick: _ => goToResource(resource), + 'data-testid': resource._self, + })} + loading={{ spinning: isLoading, indicator: <> }} + bordered={false} + ref={ref} + className={clsx( + 'data-explorer-table', + tableOffsetFromTop === FUSION_TITLEBAR_HEIGHT && + 'data-explorer-header-collapsed' + )} + rowClassName="data-explorer-row" + scroll={{ x: 'max-content' }} + locale={{ + emptyText() { + return isLoading ? <> : ; + }, + }} + pagination={tablePaginationConfig} + sticky={{ offsetHeader: tableOffsetFromTop }} + /> +
+ ); + } +); /** * For each resource in the resources array, it creates column configuration for all its keys (if the column config for that key does not already exist). diff --git a/src/subapps/dataExplorer/DateExplorerScrollArrows.tsx b/src/subapps/dataExplorer/DateExplorerScrollArrows.tsx new file mode 100644 index 000000000..5e1c83dc3 --- /dev/null +++ b/src/subapps/dataExplorer/DateExplorerScrollArrows.tsx @@ -0,0 +1,188 @@ +import React, { useEffect, useReducer, useRef } from 'react'; +import RightSpeedArrow, { + RightArrow, +} from '../../shared/components/Icons/RightSpeedArrow'; +import LeftSpeedArrow, { + LeftArrow, +} from '../../shared/components/Icons/LeftSpeedArrow'; +import { clsx } from 'clsx'; + +type TArrowsDisplay = { + returnToStart: boolean; + goToEnd: boolean; + columnsOffsetLeft: { + name: string | null; + offsetLeft: number; + width: number; + }[]; + lastColumnScrollToIndex: number; + recalculateColumnsOffsetLeft: boolean; +}; +type TDateExplorerScrollArrowsProps = { + isLoading: boolean; + container: HTMLDivElement | null; + table: HTMLDivElement | null; + orgAndProject: [string, string] | undefined; + type: string | undefined; + showEmptyDataCells: boolean; + showMetadataColumns: boolean; +}; +const DateExplorerScrollArrows = ({ + container, + table, + isLoading, + orgAndProject, + type, + showEmptyDataCells, + showMetadataColumns, +}: TDateExplorerScrollArrowsProps) => { + const [ + { + goToEnd, + returnToStart, + columnsOffsetLeft, + lastColumnScrollToIndex, + recalculateColumnsOffsetLeft, + }, + updateArrowsState, + ] = useReducer( + (previous: TArrowsDisplay, next: Partial) => ({ + ...previous, + ...next, + }), + { + returnToStart: false, + goToEnd: false, + columnsOffsetLeft: [], + lastColumnScrollToIndex: 0, + recalculateColumnsOffsetLeft: false, + } + ); + + const onLeftArrowClick = () => { + window.scrollTo({ left: 0, behavior: 'smooth' }); + }; + const onRightArrowClick = () => { + const tableRect = table?.getBoundingClientRect(); + const tableWidth = tableRect?.width || 0; + window.scrollTo({ left: tableWidth, behavior: 'smooth' }); + }; + const onLeftNormalArrowClick = () => { + const previousElement = lastColumnScrollToIndex + ? lastColumnScrollToIndex - 1 + : 0; + const previousElementOffsetLeft = columnsOffsetLeft[previousElement]; + window.scrollTo({ + left: previousElementOffsetLeft.offsetLeft + 52, + behavior: 'smooth', + }); + updateArrowsState({ lastColumnScrollToIndex: previousElement }); + }; + const onRightNormalArrowClick = () => { + const nextElement = + lastColumnScrollToIndex + 1 <= columnsOffsetLeft.length + ? lastColumnScrollToIndex + 1 + : columnsOffsetLeft.length - 1; + const nextElementOffsetLeft = columnsOffsetLeft[nextElement]; + window.scrollTo({ + left: nextElementOffsetLeft.offsetLeft + 52, + behavior: 'smooth', + }); + updateArrowsState({ lastColumnScrollToIndex: nextElement }); + }; + + useEffect(() => { + const onScroll = (ev?: Event) => { + const containerRect = container?.getBoundingClientRect(); + const tableRect = table?.getBoundingClientRect(); + const x = containerRect?.x || 0; + const width = containerRect?.width || 0; + const tableWidth = tableRect?.width || 0; + updateArrowsState({ returnToStart: x < 0 }); + updateArrowsState({ goToEnd: tableWidth > width }); + }; + if (!isLoading) { + onScroll(); + } + window.addEventListener('scroll', onScroll); + return () => window.removeEventListener('scroll', onScroll); + }, [container, table, isLoading]); + + useEffect(() => { + if (!isLoading && table && !recalculateColumnsOffsetLeft) { + const timeoutId = setTimeout(() => { + const columns = document.querySelectorAll('th.data-explorer-column'); + updateArrowsState({ + columnsOffsetLeft: Array.from(columns).map(column => { + return { + name: (column as HTMLElement).getAttribute('aria-label'), + offsetLeft: (column as HTMLElement).offsetLeft, + width: (column as HTMLElement).offsetWidth, + }; + }), + recalculateColumnsOffsetLeft: true, + }); + clearTimeout(timeoutId); + }, 1000); + } + }, [isLoading, table, recalculateColumnsOffsetLeft]); + + useEffect(() => { + window.scrollTo({ left: 0, top: 0, behavior: 'smooth' }); + updateArrowsState({ + recalculateColumnsOffsetLeft: false, + lastColumnScrollToIndex: 0, + }); + }, [orgAndProject, type, showEmptyDataCells, showMetadataColumns]); + + const tableRect = table?.getBoundingClientRect(); + const hideRightArrows = tableRect + ? tableRect.width + tableRect.x - window.innerWidth < 60 + : false; + return isLoading ? ( + <> + ) : ( +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ ); +}; + +export default DateExplorerScrollArrows; diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 7ef14836f..24dee2ed8 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -20,6 +20,7 @@ left: 20px; color: @fusion-blue-8; background: white; + &:active, &:focus { background: white; @@ -64,6 +65,7 @@ margin-left: 6px; color: @fusion-blue-8; } + .data-explorer-toggle { border: 1px solid @fusion-blue-8; box-sizing: content-box; @@ -71,12 +73,15 @@ &[aria-checked='true'] { background-color: @fusion-blue-8; + .ant-switch-handle::before { background-color: white; } } + &[aria-checked='false'] { background-color: transparent; + .ant-switch-handle::before { background-color: @fusion-blue-8; } @@ -168,6 +173,7 @@ font-weight: 700; } } + .select-menu.reduced-width { .ant-select-selector { min-width: 140px; @@ -223,6 +229,7 @@ width: auto; min-width: 90vw !important; // This is needed to make sure that the table headers and table body columns are aligned when the table is rerendered (eg when user selects a predicate, project, or, type). } + .ant-table { background: @fusion-main-bg; } @@ -264,6 +271,7 @@ .ant-select-item-option-content { color: @fusion-blue-8; width: 100%; + .first-metadata-path { width: 100%; display: block; @@ -272,3 +280,75 @@ } } } + +// style the scroll bar +::-webkit-scrollbar { + width: 0px; + height: 14px; + padding: 2px 10px; +} + +// style the scroll thumb +::-webkit-scrollbar-thumb { + background: @fusion-blue-8; + border-radius: 4px; +} + +.data-explorer-speed-arrows { + width: 100vw; + display: flex; + align-items: center; + justify-content: space-between; + position: fixed; + left: 0; + bottom: 0px; + padding: 10px; + + .left, + .right { + display: flex; + align-items: center; + justify-content: center; + } + + .left-speed-arrow, + .right-speed-arrow { + display: flex; + align-items: center; + justify-content: center; + background-color: @fusion-blue-8; + padding: 10px; + cursor: pointer; + + &--hidden { + display: none; + } + + svg { + font-size: 20px; + color: white !important; + } + } + + .left-arrow, + .right-arrow { + display: flex; + align-items: center; + justify-content: center; + background-color: @fusion-main-color; + padding: 10px; + cursor: pointer; + + &--hidden { + display: none; + } + + svg { + font-size: 20px; + color: white !important; + } + } + .right { + margin-left: auto; + } +} From b3ab9e1ee3878237336f5790a8907f81263ab82c Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 28 Jul 2023 12:53:32 +0200 Subject: [PATCH 13/20] f-4112/update: add copy button on url hover either resolvable or not --- .../components/ResourceEditor/CodeEditor.tsx | 1 + .../ResourceEditor/ResourceEditor.less | 41 +++++++++++++++++++ .../components/ResourceEditor/editorUtils.ts | 2 + .../ResourceEditor/useEditorTooltip.tsx | 41 +++++++++++++++++-- src/shared/images/confirmAnimated.svg | 1 + src/shared/images/copyColor.svg | 1 + 6 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 src/shared/images/confirmAnimated.svg create mode 100644 src/shared/images/copyColor.svg diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 2829daa60..d583c6a8a 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -38,6 +38,7 @@ const CodeEditor = forwardRef( foldCode: true, indentUnit: INDENT_UNIT, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + lineWiseCopyCut: true, extraKeys: { 'Ctrl-Q': keyFoldCode, }, diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index ebfa1d05c..5c29bc620 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -61,6 +61,36 @@ color: @error-color; } +.CodeMirror-url-copy { + cursor: pointer !important; + display: flex; + align-items: center; + justify-content: center; + z-index: 999 !important; + width: 25px !important; + height: 25px !important; + border-radius: 4px !important; + padding: 0px !important; + background-color: #fff !important; + border: 1px solid rgba(#333, 0.12) !important; + box-shadow: 0 2px 12px 0 rgba(#333, 0.12) !important; + &.copied { + width: max-content !important; + padding: 2px 4px !important; + } + .url-copy-icon { + -webkit-animation: dash-check 0.9s 0.35s ease-in-out forwards; + animation: dash-check 0.9s 0.35s ease-in-out forwards; + } + + .copied { + -webkit-animation: dash-check 0.9s 0.35s ease-in-out forwards; + animation: dash-check 0.9s 0.35s ease-in-out forwards; + color: #333 !important; + margin-left: 3px; + } +} + .code-mirror-editor { .cm-fusion-resource-link:not(.cm-property) { color: #0974ca !important; @@ -206,6 +236,7 @@ margin-left: 10px; color: @fusion-primary-color; } + .key-binding { margin-left: 10px; color: @fusion-primary-color; @@ -220,3 +251,13 @@ gap: 20px; width: 100%; } + +@-webkit-keyframes dash-check { + 0% { + stroke-dashoffset: -100; + } + + 100% { + stroke-dashoffset: 900; + } +} diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index 61f04fa56..8b8e56d36 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -48,6 +48,7 @@ type TReturnedResolvedData = Omit< export const LINE_HEIGHT = 15; export const INDENT_UNIT = 4; export const CODEMIRROR_HOVER_CLASS = 'CodeMirror-hover-tooltip'; +export const CODEMIRROR_COPY_URL_CLASS = 'CodeMirror-url-copy'; export const CODEMIRROR_LINK_CLASS = 'fusion-resource-link'; const NEAR_BY = [0, 0, 0, 5, 0, -5, 5, 0, -5, 0]; const isDownloadableLink = (resource: Resource) => { @@ -104,6 +105,7 @@ export function getTokenAndPosAt(e: MouseEvent, current: CodeMirror.Editor) { if (token && url === text) { return { url, + pos, coords: { left: editorRect.left, top: coords.top + LINE_HEIGHT, diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index 693c019cd..7f09aae8b 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -4,6 +4,7 @@ import clsx from 'clsx'; import { useNexusContext } from '@bbp/react-nexus'; import { useSelector } from 'react-redux'; import { + CODEMIRROR_COPY_URL_CLASS, CODEMIRROR_HOVER_CLASS, TEditorPopoverResolvedData, editorLinkResolutionHandler, @@ -13,9 +14,11 @@ import { import { TDELink } from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; import useResolutionActions from './useResolutionActions'; +import { triggerCopy } from '../../utils/copy'; const downloadImg = require('../../images/DownloadingLoop.svg'); - +const copyImg = require('../../images/copyColor.svg'); +const copyConfirmedImage = require('../../images/confirmAnimated.svg'); type TTooltipCreator = Pick< TEditorPopoverResolvedData, 'error' | 'resolvedAs' | 'results' @@ -34,7 +37,35 @@ function removeTooltipsFromDOM() { tooltip.remove(); }); } +function removeAllCopyFromDOM() { + const copiesBtn = document.getElementsByClassName(CODEMIRROR_COPY_URL_CLASS); + copiesBtn && + Array.from(copiesBtn).forEach(btn => { + btn.remove(); + }); +} +function createCopyButton(url: string) { + const copyBtn = document.createElement('div'); + const img = copyBtn.appendChild(document.createElement('img')); + const copied = copyBtn.appendChild(document.createElement('span')); + copyBtn.className = CODEMIRROR_COPY_URL_CLASS; + img.className = 'url-copy-icon'; + + img.src = copyImg; + copyBtn.onclick = () => { + triggerCopy(url); + img.src = copyConfirmedImage; + copied.innerText = 'copied to clipboard'; + copied.className = 'copied'; + copyBtn.classList.add('copied'); + const timeoutId = setTimeout(() => { + copyBtn.remove(); + clearTimeout(timeoutId); + }, 1000); + }; + return copyBtn; +} function createTooltipNode({ tag, title, @@ -222,14 +253,16 @@ function useEditorTooltip({ return tooltip; } - async function onMouseOver(ev: MouseEvent) { const node = ev.target as HTMLElement; if (node && !node.classList.contains('cm-property')) { - const { url } = getTokenAndPosAt(ev, currentEditor); + const { url, pos } = getTokenAndPosAt(ev, currentEditor); if (url && mayBeResolvableLink(url)) { - node.classList.add('wait-for-tooltip'); + removeAllCopyFromDOM(); removeTooltipsFromDOM(); + node.classList.add('wait-for-tooltip'); + const copyBtn = createCopyButton(url); + currentEditor.addWidget(pos!, copyBtn, false); editorLinkResolutionHandler({ nexus, apiEndpoint, diff --git a/src/shared/images/confirmAnimated.svg b/src/shared/images/confirmAnimated.svg new file mode 100644 index 000000000..7413f97bb --- /dev/null +++ b/src/shared/images/confirmAnimated.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/shared/images/copyColor.svg b/src/shared/images/copyColor.svg new file mode 100644 index 000000000..4a73574d4 --- /dev/null +++ b/src/shared/images/copyColor.svg @@ -0,0 +1 @@ + \ No newline at end of file From 566eac7d901db1d6733c113493c6aae7acba3ce3 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 28 Jul 2023 12:53:32 +0200 Subject: [PATCH 14/20] f-4112/update: add copy button on url hover either resolvable or not --- .../components/ResourceEditor/CodeEditor.tsx | 1 + .../ResourceEditor/ResourceEditor.less | 23 +++++++++++ .../components/ResourceEditor/editorUtils.ts | 2 + .../ResourceEditor/useEditorTooltip.tsx | 40 +++++++++++++++++-- src/shared/images/confirmAnimated.svg | 1 + src/shared/images/copyColor.svg | 1 + 6 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/shared/images/confirmAnimated.svg create mode 100644 src/shared/images/copyColor.svg diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 2829daa60..d583c6a8a 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -38,6 +38,7 @@ const CodeEditor = forwardRef( foldCode: true, indentUnit: INDENT_UNIT, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + lineWiseCopyCut: true, extraKeys: { 'Ctrl-Q': keyFoldCode, }, diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 03b7b9787..564eaea6f 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -61,6 +61,29 @@ color: @error-color; } +.CodeMirror-url-copy { + cursor: pointer !important; + display: flex; + align-items: center; + justify-content: center; + z-index: 999 !important; + width: 25px !important; + height: 25px !important; + border-radius: 4px !important; + padding: 0px !important; + background-color: #fff !important; + border: 1px solid rgba(#333, 0.12) !important; + box-shadow: 0 2px 12px 0 rgba(#333, 0.12) !important; + &.copied { + width: max-content !important; + padding: 2px 4px !important; + } + .copied { + color: #333 !important; + margin-left: 3px; + } +} + .code-mirror-editor { .cm-fusion-resource-link:not(.cm-property) { color: #0974ca !important; diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index 61f04fa56..8b8e56d36 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -48,6 +48,7 @@ type TReturnedResolvedData = Omit< export const LINE_HEIGHT = 15; export const INDENT_UNIT = 4; export const CODEMIRROR_HOVER_CLASS = 'CodeMirror-hover-tooltip'; +export const CODEMIRROR_COPY_URL_CLASS = 'CodeMirror-url-copy'; export const CODEMIRROR_LINK_CLASS = 'fusion-resource-link'; const NEAR_BY = [0, 0, 0, 5, 0, -5, 5, 0, -5, 0]; const isDownloadableLink = (resource: Resource) => { @@ -104,6 +105,7 @@ export function getTokenAndPosAt(e: MouseEvent, current: CodeMirror.Editor) { if (token && url === text) { return { url, + pos, coords: { left: editorRect.left, top: coords.top + LINE_HEIGHT, diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index 7cc28bf02..d77bf2f25 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -5,6 +5,7 @@ import { useNexusContext } from '@bbp/react-nexus'; import { useSelector } from 'react-redux'; import * as pluralize from 'pluralize'; import { + CODEMIRROR_COPY_URL_CLASS, CODEMIRROR_HOVER_CLASS, TEditorPopoverResolvedData, editorLinkResolutionHandler, @@ -14,9 +15,12 @@ import { import { TDELink } from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; import useResolutionActions from './useResolutionActions'; +import { triggerCopy } from '../../utils/copy'; const downloadImg = require('../../images/DownloadingLoop.svg'); const infoImg = require('../../images/InfoCircleLine.svg'); +const copyImg = require('../../images/copyColor.svg'); +const copyConfirmedImage = require('../../images/confirmAnimated.svg'); type TTooltipCreator = Pick< TEditorPopoverResolvedData, @@ -38,7 +42,35 @@ function removeTooltipsFromDOM() { tooltip.remove(); }); } +function removeAllCopyFromDOM() { + const copiesBtn = document.getElementsByClassName(CODEMIRROR_COPY_URL_CLASS); + copiesBtn && + Array.from(copiesBtn).forEach(btn => { + btn.remove(); + }); +} +function createCopyButton(url: string) { + const copyBtn = document.createElement('div'); + const img = copyBtn.appendChild(document.createElement('img')); + const copied = copyBtn.appendChild(document.createElement('span')); + copyBtn.className = CODEMIRROR_COPY_URL_CLASS; + img.className = 'url-copy-icon'; + + img.src = copyImg; + copyBtn.onclick = () => { + triggerCopy(url); + img.src = copyConfirmedImage; + copied.innerText = 'copied to clipboard'; + copied.className = 'copied'; + copyBtn.classList.add('copied'); + const timeoutId = setTimeout(() => { + copyBtn.remove(); + clearTimeout(timeoutId); + }, 1000); + }; + return copyBtn; +} function createTooltipNode({ tag, title, @@ -273,14 +305,16 @@ function useEditorTooltip({ return tooltip; } - async function onMouseOver(ev: MouseEvent) { const node = ev.target as HTMLElement; if (node && !node.classList.contains('cm-property')) { - const { url } = getTokenAndPosAt(ev, currentEditor); + const { url, pos } = getTokenAndPosAt(ev, currentEditor); if (url && mayBeResolvableLink(url)) { - node.classList.add('wait-for-tooltip'); + removeAllCopyFromDOM(); removeTooltipsFromDOM(); + node.classList.add('wait-for-tooltip'); + const copyBtn = createCopyButton(url); + currentEditor.addWidget(pos!, copyBtn, false); editorLinkResolutionHandler({ nexus, apiEndpoint, diff --git a/src/shared/images/confirmAnimated.svg b/src/shared/images/confirmAnimated.svg new file mode 100644 index 000000000..7413f97bb --- /dev/null +++ b/src/shared/images/confirmAnimated.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/shared/images/copyColor.svg b/src/shared/images/copyColor.svg new file mode 100644 index 000000000..4a73574d4 --- /dev/null +++ b/src/shared/images/copyColor.svg @@ -0,0 +1 @@ + \ No newline at end of file From 60888d1190c35b585d918094a2134c0d0e3d7b33 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 28 Jul 2023 18:25:39 +0200 Subject: [PATCH 15/20] fix: show the warning only when resolver failed --- src/shared/components/ResourceEditor/editorUtils.ts | 4 ++++ .../components/ResourceEditor/useEditorTooltip.tsx | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index 61f04fa56..6569a61e8 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -31,6 +31,7 @@ export type TEditorPopoverResolvedData = { left: number; results?: TDELink | TDELink[]; resolvedAs: TEditorPopoverResolvedAs; + resolver?: 'resolver-api' | 'search-api'; error?: any; }; type TDeltaError = Error & { @@ -187,6 +188,7 @@ export async function editorLinkResolutionHandler({ // next-action: open resource editor return { resolvedAs: 'resource', + resolver: 'resolver-api', results: { isDownloadable, _self: details._self, @@ -217,6 +219,7 @@ export async function editorLinkResolutionHandler({ const entity = getOrgAndProjectFromResourceObject(result); return { resolvedAs: 'resource', + resolver: 'search-api', results: { isDownloadable, _self: result._self, @@ -233,6 +236,7 @@ export async function editorLinkResolutionHandler({ // next-action: open resources list in the popover return { resolvedAs: 'resources', + resolver: 'search-api', results: details._results.map((item: Resource) => { const isDownloadable = isDownloadableLink(item); const entity = getOrgAndProjectFromResourceObject(item); diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index 7cc28bf02..629d9332c 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -20,7 +20,7 @@ const infoImg = require('../../images/InfoCircleLine.svg'); type TTooltipCreator = Pick< TEditorPopoverResolvedData, - 'error' | 'resolvedAs' | 'results' + 'error' | 'resolvedAs' | 'results' | 'resolver' > & { onDownload?: () => void; }; @@ -119,6 +119,7 @@ function createTooltipContent({ error, results, onDownload, + resolver, }: TTooltipCreator) { const tooltipContent = document.createElement('div'); tooltipContent.className = clsx( @@ -136,8 +137,10 @@ function createTooltipContent({ } if (resolvedAs === 'resource') { const result = results as TDELink; - const warningHeader = createWarningHeader(1); - tooltipContent.appendChild(warningHeader); + if (resolver === 'search-api') { + const warningHeader = createWarningHeader(1); + tooltipContent.appendChild(warningHeader); + } tooltipContent.appendChild( createTooltipNode({ onDownload, @@ -287,11 +290,12 @@ function useEditorTooltip({ url, orgLabel, projectLabel, - }).then(({ resolvedAs, results, error }) => { + }).then(({ resolvedAs, results, error, resolver }) => { const tooltipContent = createTooltipContent({ resolvedAs, error, results, + resolver, onDownload: resolvedAs === 'resource' && (results as TDELink).isDownloadable ? () => { From 901d0600aa6f6e9ff0e507866b6c69d90bcca6f8 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 28 Jul 2023 18:58:11 +0200 Subject: [PATCH 16/20] fix: use hard encoded resource id --- .../containers/ResourceActionsContainer.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/shared/containers/ResourceActionsContainer.tsx b/src/shared/containers/ResourceActionsContainer.tsx index d19e1d477..e4a0c34e2 100644 --- a/src/shared/containers/ResourceActionsContainer.tsx +++ b/src/shared/containers/ResourceActionsContainer.tsx @@ -19,7 +19,7 @@ import { } from '../utils/nexusMaybe'; import useNotification from '../hooks/useNotification'; import RemoveTagButton from './RemoveTagButtonContainer'; -import { parseResourceId } from '../components/Preview/Preview'; +import nexusUrlHardEncode from '../../shared/utils/nexusEncode'; const ResourceActionsContainer: React.FunctionComponent<{ resource: Resource; @@ -154,15 +154,14 @@ const ResourceActionsContainer: React.FunctionComponent<{ }, downloadFile: async () => { try { - const data = await nexus.httpGet({ - path: resource._self, - headers: { - Accept: 'application/json', - }, - context: { + const data = await nexus.File.get( + orgLabel, + projectLabel, + nexusUrlHardEncode(resourceId), + { as: 'blob', - }, - }); + } + ); return download( resource._filename || getResourceLabel(resource), resource._mediaType, From 1b78699ead1ca6f595d62240c75e15e0d221cf1e Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 24 Jul 2023 12:42:26 +0200 Subject: [PATCH 17/20] f-4094/update: add data cart logic to allow the user to download resources --- .../dataExplorer/DataExplorerTable.tsx | 191 +++++++++++++++++- src/subapps/dataExplorer/styles.less | 1 + 2 files changed, 182 insertions(+), 10 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index 91264792d..eca73ecfa 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -1,17 +1,37 @@ +import React, { useEffect, useReducer, forwardRef } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; import { Resource } from '@bbp/nexus-sdk'; import { Empty, Table, Tooltip } from 'antd'; import { ColumnType, TablePaginationConfig } from 'antd/lib/table'; -import { isArray, isString, startCase } from 'lodash'; -import React, { forwardRef } from 'react'; -import { makeOrgProjectTuple } from '../../shared/molecules/MyDataTable/MyDataTable'; + +import { isArray, isNil, isString, startCase } from 'lodash'; +import { SelectionSelectFn } from 'antd/lib/table/interface'; +import { clsx } from 'clsx'; + +import { + MAX_DATA_SELECTED_SIZE__IN_BYTES, + MAX_LOCAL_STORAGE_ALLOWED_SIZE, + TDataSource, + TResourceTableData, + getLocalStorageSize, + makeOrgProjectTuple, + notifyTotalSizeExeeced, +} from '../../shared/molecules/MyDataTable/MyDataTable'; import isValidUrl from '../../utils/validUrl'; import { NoDataCell } from './NoDataCell'; -import './styles.less'; import { DataExplorerConfiguration } from './DataExplorer'; -import { useHistory, useLocation } from 'react-router-dom'; import { makeResourceUri, parseProjectUrl } from '../../shared/utils'; -import { clsx } from 'clsx'; import { FUSION_TITLEBAR_HEIGHT } from './DataExplorerCollapsibleHeader'; +import { + DATA_PANEL_STORAGE, + DATA_PANEL_STORAGE_EVENT, + DataPanelEvent, +} from '../../shared/organisms/DataPanel/DataPanel'; +import { + removeLocalStorageRows, + toLocalStorageResources, +} from '../../shared/utils/datapanel'; +import './styles.less'; interface TDataExplorerTable { isLoading: boolean; @@ -25,8 +45,13 @@ interface TDataExplorerTable { tableOffsetFromTop: number; } -type TColumnNameToConfig = Map>; +type TDateExplorerTableData = { + selectedRowKeys: React.Key[]; + selectedRows: TDataSource[]; +}; +type TColumnNameToConfig = Map>; +const DATA_EXPLORER_NAMESPACE = 'data-explorer'; export const DataExplorerTable = forwardRef( ( { @@ -44,9 +69,20 @@ export const DataExplorerTable = forwardRef( ) => { const history = useHistory(); const location = useLocation(); - + const [{ selectedRowKeys }, updateTableData] = useReducer( + ( + previous: TResourceTableData, + partialData: Partial + ) => ({ + ...previous, + ...partialData, + }), + { + selectedRowKeys: [], + selectedRows: [], + } + ); const allowedTotal = total ? (total > 10000 ? 10000 : total) : undefined; - const tablePaginationConfig: TablePaginationConfig = { pageSize, total: allowedTotal, @@ -72,6 +108,136 @@ export const DataExplorerTable = forwardRef( background: location, }); }; + const onSelectRowChange: SelectionSelectFn = async ( + record, + selected + ) => { + const recordKey = record._self; + const dataPanelLS: TDateExplorerTableData = JSON.parse( + localStorage.getItem(DATA_PANEL_STORAGE)! + ); + + const localStorageRows = toLocalStorageResources( + record, + DATA_EXPLORER_NAMESPACE + ); + let selectedRowKeys = dataPanelLS?.selectedRowKeys || []; + let selectedRows = dataPanelLS?.selectedRows || []; + + if (selected) { + selectedRowKeys = [...selectedRowKeys, recordKey]; + selectedRows = [...selectedRows, ...localStorageRows]; + } else { + selectedRowKeys = selectedRowKeys.filter(t => t !== recordKey); + selectedRows = removeLocalStorageRows(selectedRows, [recordKey]); + } + + const size = selectedRows.reduce( + (acc, item) => acc + (item.distribution?.contentSize || 0), + 0 + ); + if ( + size > MAX_DATA_SELECTED_SIZE__IN_BYTES || + getLocalStorageSize() > MAX_LOCAL_STORAGE_ALLOWED_SIZE + ) { + return notifyTotalSizeExeeced(); + } + localStorage.setItem( + DATA_PANEL_STORAGE, + JSON.stringify({ + selectedRowKeys, + selectedRows, + }) + ); + window.dispatchEvent( + new CustomEvent(DATA_PANEL_STORAGE_EVENT, { + detail: { + datapanel: { selectedRowKeys, selectedRows }, + }, + }) + ); + }; + const onSelectAllChange = async ( + selected: boolean, + tSelectedRows: Resource[], + changeRows: Resource[] + ) => { + const dataPanelLS: TDateExplorerTableData = JSON.parse( + localStorage.getItem(DATA_PANEL_STORAGE)! + ); + let selectedRowKeys = dataPanelLS?.selectedRowKeys || []; + let selectedRows = dataPanelLS?.selectedRows || []; + + if (selected) { + const results = changeRows.map(row => + toLocalStorageResources(row, DATA_EXPLORER_NAMESPACE) + ); + selectedRows = [...selectedRows, ...results.flat()]; + selectedRowKeys = [...selectedRowKeys, ...changeRows.map(t => t._self)]; + } else { + const rowKeysToRemove = changeRows.map(r => r._self); + + selectedRowKeys = selectedRowKeys.filter( + key => !rowKeysToRemove.includes(key.toString()) + ); + selectedRows = removeLocalStorageRows(selectedRows, rowKeysToRemove); + } + const size = selectedRows.reduce( + (acc, item) => acc + (item.distribution?.contentSize || 0), + 0 + ); + if ( + size > MAX_DATA_SELECTED_SIZE__IN_BYTES || + getLocalStorageSize() > MAX_LOCAL_STORAGE_ALLOWED_SIZE + ) { + return notifyTotalSizeExeeced(); + } + localStorage.setItem( + DATA_PANEL_STORAGE, + JSON.stringify({ + selectedRowKeys, + selectedRows, + }) + ); + window.dispatchEvent( + new CustomEvent(DATA_PANEL_STORAGE_EVENT, { + detail: { + datapanel: { selectedRowKeys, selectedRows }, + }, + }) + ); + }; + + useEffect(() => { + const dataLs = localStorage.getItem(DATA_PANEL_STORAGE); + const dataLsObject: TResourceTableData = JSON.parse(dataLs as string); + if (dataLs && dataLs.length) { + updateTableData({ + selectedRows: dataLsObject.selectedRows, + selectedRowKeys: dataLsObject.selectedRowKeys, + }); + } + }, []); + useEffect(() => { + const dataPanelEventListner = ( + event: DataPanelEvent<{ datapanel: TResourceTableData }> + ) => { + updateTableData({ + selectedRows: event.detail?.datapanel.selectedRows, + selectedRowKeys: event.detail?.datapanel.selectedRowKeys, + }); + }; + window.addEventListener( + DATA_PANEL_STORAGE_EVENT, + dataPanelEventListner as EventListener + ); + return () => { + window.removeEventListener( + DATA_PANEL_STORAGE_EVENT, + dataPanelEventListner as EventListener + ); + }; + }, []); return (
( }} > + ref={ref} columns={columnsConfig(columns, showEmptyDataCells)} dataSource={dataSource} rowKey={record => record._self} @@ -96,7 +263,6 @@ export const DataExplorerTable = forwardRef( })} loading={{ spinning: isLoading, indicator: <> }} bordered={false} - ref={ref} className={clsx( 'data-explorer-table', tableOffsetFromTop === FUSION_TITLEBAR_HEIGHT && @@ -109,6 +275,11 @@ export const DataExplorerTable = forwardRef( return isLoading ? <> : ; }, }} + rowSelection={{ + selectedRowKeys, + onSelect: onSelectRowChange, + onSelectAll: onSelectAllChange, + }} pagination={tablePaginationConfig} sticky={{ offsetHeader: tableOffsetFromTop }} /> diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 45deebac6..cff2bf5a1 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -225,6 +225,7 @@ } .data-explorer-table { + margin-bottom: 60px; table { width: auto; min-width: 90vw !important; // This is needed to make sure that the table headers and table body columns are aligned when the table is rerendered (eg when user selects a predicate, project, or, type). From ee988c973286155e7ff43cdb93275ceccbefc746 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 31 Jul 2023 12:22:01 +0200 Subject: [PATCH 18/20] f-4094/update: update jest config --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e1a1e38df..ed57fe8ee 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "NODE_ENV=development DEBUG=* webpack --mode development --config-name server && node dist/server.js", "build": "NODE_ENV=production NODE_OPTIONS=--max_old_space_size=8192 webpack --mode production", "test": "jest", - "test:watch": "jest --watch", + "test:watch": "jest --watch --maxworkers=4", "cy:open": "cypress open", "cy:run": "cypress run", "test-ui": "API_ENDPOINT=http://test start-server-and-test start http://localhost:8000 cy:run", @@ -211,7 +211,10 @@ ], "globals": { "FUSION_VERSION": "1.0.0", - "COMMIT_HASH": "9013fa343" + "COMMIT_HASH": "9013fa343", + "ts-jest": { + "isolatedModules": true + } }, "watchPathIgnorePatterns": [ "node_modules" From 7e77559a27072a921826cb755c7641d5e3e72721 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 31 Jul 2023 20:45:06 +0200 Subject: [PATCH 19/20] f-4094/update: test project selector --- src/subapps/dataExplorer/DataExplorer.spec.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index c1b22829c..6a075233b 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -38,6 +38,7 @@ import { Provider } from 'react-redux'; import configureStore from '../../shared/store'; import { ALWAYS_DISPLAYED_COLUMNS, isNexusMetadata } from './DataExplorerUtils'; +window.scrollTo = jest.fn(); describe('DataExplorer', () => { const defaultTotalResults = 500_123; const mockResourcesOnPage1: Resource[] = getCompleteResources(); @@ -195,7 +196,7 @@ describe('DataExplorer', () => { }; const projectFromRow = (row: Element) => { - const projectColumn = row.querySelector('td'); // first column is the project column + const projectColumn = row.querySelector('td.data-explorer-column-_project'); // first column is the project column return projectColumn?.textContent; }; From 152d0a71968cd3f76060433a398b4330efb111bb Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 4 Aug 2023 09:55:26 +0200 Subject: [PATCH 20/20] fix: number of workers for jest --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ed57fe8ee..7df9091c1 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "NODE_ENV=development DEBUG=* webpack --mode development --config-name server && node dist/server.js", "build": "NODE_ENV=production NODE_OPTIONS=--max_old_space_size=8192 webpack --mode production", "test": "jest", - "test:watch": "jest --watch --maxworkers=4", + "test:watch": "jest --watch --maxWorkers=4", "cy:open": "cypress open", "cy:run": "cypress run", "test-ui": "API_ENDPOINT=http://test start-server-and-test start http://localhost:8000 cy:run",