From 1e099290935673a56ce7b99530cd0bf78c40005a Mon Sep 17 00:00:00 2001 From: Bilal Meddah Date: Tue, 28 May 2024 12:00:36 +0200 Subject: [PATCH] update: fix auto scroll on delete search string update: refactor content of search body area --- .../components/FullTextSearch/index.tsx | 91 ++- .../components/FullTextSearch/styles.scss | 20 +- src/shared/components/Header/Header.scss | 24 + src/shared/components/Header/Header.tsx | 21 +- src/shared/components/Preview/Preview.tsx | 628 +++++++++--------- src/shared/lib.scss | 9 +- 6 files changed, 432 insertions(+), 361 deletions(-) diff --git a/src/shared/components/FullTextSearch/index.tsx b/src/shared/components/FullTextSearch/index.tsx index 722635f8c..bdc3d24bc 100644 --- a/src/shared/components/FullTextSearch/index.tsx +++ b/src/shared/components/FullTextSearch/index.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { useQuery } from 'react-query'; import CommandPalette from 'react-cmdk'; import { useNexusContext } from '@bbp/react-nexus'; -import { Tag } from 'antd'; +import { Spin, Tag, Empty } from 'antd'; import { groupBy } from 'lodash'; import { @@ -13,6 +13,7 @@ import { } from 'shared/utils'; import 'react-cmdk/dist/cmdk.css'; import './styles.scss'; +import { LoadingOutlined } from '@ant-design/icons'; type Props = { openCmdk: boolean; @@ -37,7 +38,8 @@ export function useFullTextSearch() { const [search, setSearch] = useState(''); const nexus = useNexusContext(); - const onSearch = async (value: string) => setSearch(value); + const onSearch = (value: string) => setSearch(value); + const resetSearch = () => setSearch(''); const { isLoading, data } = useQuery({ enabled: !!search, @@ -64,18 +66,69 @@ export function useFullTextSearch() { return { search, onSearch, + resetSearch, isLoading, searchResults, }; } const FullTextSearch = ({ openCmdk, onOpenCmdk }: Props) => { - const { search, onSearch, searchResults } = useFullTextSearch(); + const { + search, + onSearch, + resetSearch, + searchResults, + isLoading, + } = useFullTextSearch(); + + const onChangeOpen = () => { + onOpenCmdk(); + resetSearch(); + }; + + let content = null; + + if (isLoading) { + content = ( +
+ } /> +
+ ); + } else if (!Boolean(searchResults.length) && !Boolean(search)) { + content =
Search for ""
; + } else if (!Boolean(searchResults.length) && Boolean(search)) { + content = ; + } else { + content = searchResults.map(({ id, title, items }) => ( +
+
+ {title?.orgLabel}| + {title?.projectLabel} +
+
+ {items.map(resource => { + const label = getResourceLabel(resource); + return ( + +
{label}
+ + + ); + })} +
+
+ )); + } useEffect(() => { const keyDown = (e: KeyboardEvent) => { - if (e.key === '/') { + if (e.ctrlKey && e.key === 'e') { e.preventDefault(); + e.stopPropagation(); onOpenCmdk(); } }; @@ -87,37 +140,13 @@ const FullTextSearch = ({ openCmdk, onOpenCmdk }: Props) => { return ( - - {searchResults.map(({ id, title, items }) => ( -
-
- {title?.orgLabel}| - {title?.projectLabel} -
-
- {items.map(resource => { - const label = getResourceLabel(resource); - return ( - -
{label}
- - - ); - })} -
-
- ))} -
- + {content}
); }; diff --git a/src/shared/components/FullTextSearch/styles.scss b/src/shared/components/FullTextSearch/styles.scss index dea0faac4..63ded4ffc 100644 --- a/src/shared/components/FullTextSearch/styles.scss +++ b/src/shared/components/FullTextSearch/styles.scss @@ -33,8 +33,8 @@ border: 1px solid #efefef; &:hover { - background: $fusion-secondary-color !important; - box-shadow: 0 2px 12px 0px rgba($color: #333, $alpha: 0.12); + background: $fusion-blue-1 !important; + box-shadow: 0 2px 12px 0px rgba($color: #333, $alpha: 0.14); } } @@ -52,3 +52,19 @@ .item-creation_date { font-size: 12px; } + +.search-resources-loader { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding-top: 10px; + padding-bottom: 10px; +} + +.search-for-placeholder { + background-color: $fusion-neutral-2; + padding: 10px; + color: #333; + user-select: none; +} diff --git a/src/shared/components/Header/Header.scss b/src/shared/components/Header/Header.scss index a16de3f05..21b07bf33 100644 --- a/src/shared/components/Header/Header.scss +++ b/src/shared/components/Header/Header.scss @@ -240,4 +240,28 @@ cursor: pointer; border-radius: 4px; color: #efefef; + display: flex; + gap: 2px; + align-items: center; + justify-content: center; + min-width: max-content; + position: relative; + background-color: white; + color: #33333385; + + &-input { + position: absolute; + inset: 0; + } + &-btn { + position: relative; + width: max-content; + min-width: max-content; + } + kbd { + box-shadow: none; + background-color: transparent; + margin-left: 2px; + margin-right: 2px; + } } diff --git a/src/shared/components/Header/Header.tsx b/src/shared/components/Header/Header.tsx index 449b317bd..4b4363000 100644 --- a/src/shared/components/Header/Header.tsx +++ b/src/shared/components/Header/Header.tsx @@ -2,6 +2,7 @@ import React, { useReducer } from 'react'; import { Link } from 'react-router-dom'; import { useLocation } from 'react-router'; import { Menu, Dropdown, MenuItemProps } from 'antd'; + import { UserOutlined, BookOutlined, @@ -12,6 +13,7 @@ import { CopyOutlined, MenuOutlined, PlusOutlined, + SearchOutlined, } from '@ant-design/icons'; import { useDispatch, useSelector } from 'react-redux'; import { UISettingsActionTypes } from '../../store/actions/ui-settings'; @@ -200,13 +202,18 @@ const Header: React.FunctionComponent = ({ {token ? (
- +
+ +
+ Click on +
+ Ctrl+e +
+ to search +
{name && } {name && showCreationPanel && (
{ - const { apiEndpoint } = useSelector((state: RootState) => state.config); - const notification = useNotification(); - const [selectedRows, setSelectedRows] = React.useState([]); - const [previewAsset, setPreviewAsset] = React.useState(); - const [orgLabel, projectLabel] = parseProjectUrl(resource._project); - const renderFileSize = (contentSize: { - value: string; - unitCode?: string; - }) => { - if (!contentSize) { - return '-'; + const { apiEndpoint } = useSelector((state: RootState) => state.config); + const notification = useNotification(); + const [selectedRows, setSelectedRows] = React.useState([]); + const [previewAsset, setPreviewAsset] = React.useState(); + const [orgLabel, projectLabel] = parseProjectUrl(resource._project); + const renderFileSize = (contentSize: { + value: string; + unitCode?: string; + }) => { + if (!contentSize) { + return '-'; + } + if (contentSize.unitCode) { + if (contentSize.unitCode.toLocaleLowerCase() === 'kilo bytes') { + return `${contentSize.value} KB`; } - if (contentSize.unitCode) { - if (contentSize.unitCode.toLocaleLowerCase() === 'kilo bytes') { - return `${contentSize.value} KB`; - } - if (contentSize.unitCode.toLocaleLowerCase() === 'mega bytes') { - return `${contentSize.value} MB`; - } - } - const sizeInMB = (parseInt(contentSize.value, 10) / 1000000).toFixed(2); - if (sizeInMB !== '0.00') { - return `${sizeInMB} MB`; + if (contentSize.unitCode.toLocaleLowerCase() === 'mega bytes') { + return `${contentSize.value} MB`; } + } + const sizeInMB = (parseInt(contentSize.value, 10) / 1000000).toFixed(2); + if (sizeInMB !== '0.00') { + return `${sizeInMB} MB`; + } - return `${contentSize.value} Bytes`; - }; - - const isNexusFile = (url: string) => { - const resourceId = parseResourceId(url); - return resourceId !== ''; - }; - - const columns = [ - { - title: 'Name', - dataIndex: 'name', - key: 'name', - render: (text: string) => text || '-', - }, - { - title: 'Asset Type / Encoding Format', - dataIndex: 'encodingFormat', - key: 'encodingFormat', - render: (text: string) => text || '-', - }, - { - title: 'File Size', - dataIndex: 'contentSize', - key: 'contentSize', - render: renderFileSize, - }, - { - title: 'Actions', - dataIndex: 'asset', - key: 'actions', - render: (asset: { - url: string; - name: string; - encodingFormat: string; - }) => { - return ( - - - - - - - - - - - - ); - }, - }, - ]; + return `${contentSize.value} Bytes`; + }; - const downloadMultipleFiles = async () => { - const resourcesPayload = selectedRows - .map(row => { - return row.asset.url; - }) - .map(url => { - const resourceId = parseResourceId(url); - return { - resourceId, - '@type': 'File', - project: `${orgLabel}/${projectLabel}`, - }; - }); - const archiveId = uuidv4(); - const payload: ArchivePayload = { - archiveId, - resources: resourcesPayload, - }; + const isNexusFile = (url: string) => { + const resourceId = parseResourceId(url); + return resourceId !== ''; + }; - try { - // TODO: fix the SDK to handle empty response - await fetch( - `${apiEndpoint}/archives/${orgLabel}/${projectLabel}/${payload.archiveId}`, - { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${localStorage.getItem('nexus__token')}`, - }, - body: JSON.stringify(payload), - } + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + render: (text: string) => text || '-', + }, + { + title: 'Asset Type / Encoding Format', + dataIndex: 'encodingFormat', + key: 'encodingFormat', + render: (text: string) => text || '-', + }, + { + title: 'File Size', + dataIndex: 'contentSize', + key: 'contentSize', + render: renderFileSize, + }, + { + title: 'Actions', + dataIndex: 'asset', + key: 'actions', + render: (asset: { + url: string; + name: string; + encodingFormat: string; + }) => { + return ( + + + + + + + + + + + ); - const archive = await nexus.httpGet({ - path: `${apiEndpoint}/archives/${orgLabel}/${projectLabel}/${payload.archiveId}?ignoreNotFound=true`, - headers: { accept: 'application/zip, application/json' }, - context: { - parseAs: 'blob', - }, - }); - const blob = archive as Blob; - const archiveName = `data-${payload.archiveId}.zip`; - downloadBlobHelper(blob, archiveName); + }, + }, + ]; - notification.success({ - message: `Archive ${archiveName} downloaded successfully`, - }); - } catch (error) { - Sentry.captureException({ error, message: 'Failed to download archive' }); - notification.error({ - message: 'Failed to download the file', - description: (error as TError).reason || (error as TError).message, - }); - } + const downloadMultipleFiles = async () => { + const resourcesPayload = selectedRows + .map(row => { + return row.asset.url; + }) + .map(url => { + const resourceId = parseResourceId(url); + return { + resourceId, + '@type': 'File', + project: `${orgLabel}/${projectLabel}`, + }; + }); + const archiveId = uuidv4(); + const payload: ArchivePayload = { + archiveId, + resources: resourcesPayload, }; - const downloadButton = (disabled: boolean) => { - const isDisabled = disabled || selectedRows.length <= 0; - const disabledToolTip = - selectedRows.length <= 0 - ? 'Please Select files to download' - : 'You don’t have the required permissions to create an archive for some of the selected resources. Please contact your project administrator to request to be granted the required archives/write permission.'; - const btn = ( - + try { + // TODO: fix the SDK to handle empty response + await fetch( + `${apiEndpoint}/archives/${orgLabel}/${projectLabel}/${payload.archiveId}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('nexus__token')}`, + }, + body: JSON.stringify(payload), + } ); + const archive = await nexus.httpGet({ + path: `${apiEndpoint}/archives/${orgLabel}/${projectLabel}/${payload.archiveId}?ignoreNotFound=true`, + headers: { accept: 'application/zip, application/json' }, + context: { + parseAs: 'blob', + }, + }); + const blob = archive as Blob; + const archiveName = `data-${payload.archiveId}.zip`; + downloadBlobHelper(blob, archiveName); - if (isDisabled) { - return {btn}; - } + notification.success({ + message: `Archive ${archiveName} downloaded successfully`, + }); + } catch (error) { + Sentry.captureException({ error, message: 'Failed to download archive' }); + notification.error({ + message: 'Failed to download the file', + description: (error as TError).reason || (error as TError).message, + }); + } + }; - return btn; - }; + const downloadButton = (disabled: boolean) => { + const isDisabled = disabled || selectedRows.length <= 0; + const disabledToolTip = + selectedRows.length <= 0 + ? 'Please Select files to download' + : 'You don’t have the required permissions to create an archive for some of the selected resources. Please contact your project administrator to request to be granted the required archives/write permission.'; + const btn = ( + + ); - const copyURI = (id: string) => { - try { - navigator.clipboard.writeText(id); - notification.success({ message: 'URL Copied to clipboard' }); - } catch { - notification.error({ - message: 'Failed to copy the url', - }); - } - }; + if (isDisabled) { + return {btn}; + } - const downloadSingleFile = async ( - nexus: NexusClient, - orgLabel: string, - projectLabel: string, - asset: { url: string; name: string } - ) => { - const resourceId = parseResourceId(asset.url); - let contentUrl = resourceId; - const options: GetFileOptions = { - as: 'blob', - }; + return btn; + }; - if (resourceId.includes('?rev=')) { - const [url, rev] = resourceId.split('?rev='); - contentUrl = url; - options.rev = parseInt(rev, 10); - } + const copyURI = (id: string) => { + try { + navigator.clipboard.writeText(id); + notification.success({ message: 'URL Copied to clipboard' }); + } catch { + notification.error({ + message: 'Failed to copy the url', + }); + } + }; - try { - const rawData = await nexus.File.get( - orgLabel, - projectLabel, - nexusUrlHardEncode(contentUrl), - options - ); - downloadBlobHelper(rawData, asset.name); - } catch (error) { - notification.error({ - message: 'Failed to download the file', - description: (error as TError).reason || (error as TError).message, - }); - } + const downloadSingleFile = async ( + nexus: NexusClient, + orgLabel: string, + projectLabel: string, + asset: { url: string; name: string } + ) => { + const resourceId = parseResourceId(asset.url); + let contentUrl = resourceId; + const options: GetFileOptions = { + as: 'blob', }; - const downloadBlobHelper = ( - rawData: string | NexusFile | Blob | FormData, - name: string - ) => { - const blob = new Blob([rawData as string], { - type: 'octet/stream', + if (resourceId.includes('?rev=')) { + const [url, rev] = resourceId.split('?rev='); + contentUrl = url; + options.rev = parseInt(rev, 10); + } + + try { + const rawData = await nexus.File.get( + orgLabel, + projectLabel, + nexusUrlHardEncode(contentUrl), + options + ); + downloadBlobHelper(rawData, asset.name); + } catch (error) { + notification.error({ + message: 'Failed to download the file', + description: (error as TError).reason || (error as TError).message, }); - const src = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.download = name; - document.body.appendChild(a); - a.href = src; - a.click(); - URL.revokeObjectURL(src); - }; + } + }; - const getResourceAssets = (resource: Resource) => { - let data: any = []; + const downloadBlobHelper = ( + rawData: string | NexusFile | Blob | FormData, + name: string + ) => { + const blob = new Blob([rawData as string], { + type: 'octet/stream', + }); + const src = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.download = name; + document.body.appendChild(a); + a.href = src; + a.click(); + URL.revokeObjectURL(src); + }; - /* get assets dependening on type of resource */ - if (resource.distribution) { - const { distribution } = resource; - data = data.concat( - [distribution].flat().map((d, i) => { - return { - key: i, - name: - d.name || - (d.repository && (d.repository.name || d.repository['@id'])), - asset: { - url: d.contentUrl || d.url, - name: d.name, - encodingFormat: d.encodingFormat, - }, - encodingFormat: - d.encodingFormat || - (d.name?.includes('.') ? d.name.split('.').pop() : '-'), - contentSize: d.contentSize, - }; - }) - ); - } + const getResourceAssets = (resource: Resource) => { + let data: any = []; - return data; - }; + /* get assets dependening on type of resource */ + if (resource.distribution) { + const { distribution } = resource; + data = data.concat( + [distribution].flat().map((d, i) => { + return { + key: i, + name: + d.name || + (d.repository && (d.repository.name || d.repository['@id'])), + asset: { + url: d.contentUrl || d.url, + name: d.name, + encodingFormat: d.encodingFormat, + }, + encodingFormat: + d.encodingFormat || + (d.name?.includes('.') ? d.name.split('.').pop() : '-'), + contentSize: d.contentSize, + }; + }) + ); + } - const fileFormat = - previewAsset && previewAsset.name?.includes('.') - ? previewAsset.name.split('.').pop() - : '-'; + return data; + }; - return ( -
- {previewAsset && previewAsset.encodingFormat === 'application/pdf' && ( - setPreviewAsset(undefined)} - /> - )} - {previewAsset && (fileFormat === 'csv' || fileFormat === 'tsv') && ( - setPreviewAsset(undefined)} - /> - )} - - downloadButton(true)} - loadingComponent={downloadButton(false)} - > - {downloadButton(false)} - - { - if (selected) { - setSelectedRows([...selectedRows, record]); - } else { - const currentRows = selectedRows.filter( - s => s.key !== record.key - ); - setSelectedRows(currentRows); - } - }, - onSelectAll: (select, selectedRows) => { - if (select) { - setSelectedRows(selectedRows); - } else { - setSelectedRows([]); - } - }, - }} - columns={columns} - dataSource={getResourceAssets(resource)} - bordered={true} - >
- - ), - }, - ]} + const fileFormat = + previewAsset && previewAsset.name?.includes('.') + ? previewAsset.name.split('.').pop() + : '-'; + + return ( +
+ {previewAsset && previewAsset.encodingFormat === 'application/pdf' && ( + setPreviewAsset(undefined)} /> -
- ); - }; + )} + {previewAsset && (fileFormat === 'csv' || fileFormat === 'tsv') && ( + setPreviewAsset(undefined)} + /> + )} + + downloadButton(true)} + loadingComponent={downloadButton(false)} + > + {downloadButton(false)} + + { + if (selected) { + setSelectedRows([...selectedRows, record]); + } else { + const currentRows = selectedRows.filter( + s => s.key !== record.key + ); + setSelectedRows(currentRows); + } + }, + onSelectAll: (select, selectedRows) => { + if (select) { + setSelectedRows(selectedRows); + } else { + setSelectedRows([]); + } + }, + }} + columns={columns} + dataSource={getResourceAssets(resource)} + bordered={true} + >
+ + ), + }, + ]} + /> +
+ ); +}; export default Preview; diff --git a/src/shared/lib.scss b/src/shared/lib.scss index 6eed8495f..1cecf878f 100644 --- a/src/shared/lib.scss +++ b/src/shared/lib.scss @@ -22,8 +22,6 @@ $fusion-blue-4: #40a9ff; $fusion-blue-8: #003a8c; $fusion-component-background-color: #f2f2f2; $fusion-warning-color: #faad14; -$fusion-blue-1: #e6f7ff; -$fusion-blue-1: #e6f7ff; $fusion-gray-4: #d9d9d9; $fusion-daybreak-3: #91d5ff; $fusion-daybreak-4: #69c0ff; @@ -93,7 +91,6 @@ $font-family: 'Titillium Web', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - $primary-color: $fusion-primary-color; // primary color for all components $link-color: $fusion-active-link-color; // link color $link-hover-color: $fusion-hover-link-color; @@ -137,7 +134,6 @@ $component-background: $fusion-component-background-color; 0 0 0 1px rgba(9, 30, 66, 0.08); } - @mixin unstyleList { list-style: none; } @@ -214,8 +210,8 @@ $component-background: $fusion-component-background-color; :root { --font-family: 'Titillium Web', -apple-system, BlinkMacSystemFont, 'Segoe UI', - Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', - 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } // Top-level styles @@ -245,7 +241,6 @@ h1 { flex-direction: row; } - .ant-tag-blue { background-color: fade($fusion-secondary-color, 10%); color: $fusion-secondary-color;