From 974a0f81583907f77b37a72e502b68c10ac3e5e6 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:29:48 +0200 Subject: [PATCH 001/192] de-10: add navigation back button --- .../NavigationBack.tsx | 26 +++++++++++++++++++ .../DataExplorerGraphFlowMolecules/index.ts | 1 + 2 files changed, 27 insertions(+) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx new file mode 100644 index 000000000..7ae4f8ce3 --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { ReturnBackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +import { RootState } from '../../store/reducers'; +import './styles.less'; + +const NavigationBack = () => { + const dispatch = useDispatch(); + const { links } = useSelector((state: RootState) => state.dataExplorer); + const onBack = () => dispatch(ReturnBackDataExplorerGraphFlow()); + if (links.length) { + return ( + + ); + } + return null; +}; + +export default NavigationBack; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index 41a847ac9..ffb1961ed 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1 +1,2 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; +export { default as NavigationBack } from './NavigationBack'; From cea1096c55ad981b465eb38e11f22ec115ceb74d Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:30:43 +0200 Subject: [PATCH 002/192] de-10: add navigation stack item --- .../NavigationStackItem.tsx | 66 +++++++++++++++++++ .../DataExplorerGraphFlowMolecules/index.ts | 1 + 2 files changed, 67 insertions(+) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx new file mode 100644 index 000000000..2b9f94fea --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Tag, Tooltip } from 'antd'; +import { clsx } from 'clsx'; +import { isArray } from 'lodash'; +import { PlusOutlined } from '@ant-design/icons'; +import { + TDEResource, + JumpToNodeDataExplorerGrpahFlow, +} from '../../store/reducers/data-explorer'; +import { RootState } from '../../store/reducers'; +import './styles.less'; + +export type TNavigationStackItem = { + _self: string; + index: number; + types?: string | string[]; + title: string; + resource?: TDEResource; + highlighted: boolean; +}; + +const NavigationStackItem = ({ + index, + _self, + title, + types, + resource, + highlighted, +}: TNavigationStackItem) => { + const dispatch = useDispatch(); + const { shrinked, links } = useSelector( + (state: RootState) => state.dataExplorer + ); + const onClick = () => dispatch(JumpToNodeDataExplorerGrpahFlow(index)); + + return ( +
+ + {`${resource?.[0]}/${resource?.[1]}`} + {decodeURIComponent(_self)} +
+ } + > + + + {`${resource?.[0]}/${resource?.[1]}`} +
{title}
+ {types && ( +
{isArray(types) ? types.join(', ') : types}
+ )} + + ); +}; + +export default NavigationStackItem; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index ffb1961ed..bc9abbcc8 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1,2 +1,3 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; export { default as NavigationBack } from './NavigationBack'; +export { default as NavigationStackItem } from './NavigationStackItem'; From c64749ca724125b48fff98ed332cb221423576ef Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:31:36 +0200 Subject: [PATCH 003/192] de-10: add navigation stack shrinked item --- .../NavigationStackShrinkedItem.tsx | 41 +++++++++++++++++++ .../DataExplorerGraphFlowMolecules/index.ts | 1 + 2 files changed, 42 insertions(+) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx new file mode 100644 index 000000000..b0a460ac3 --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Tooltip } from 'antd'; +import { clsx } from 'clsx'; +import { MoreOutlined, PlusOutlined } from '@ant-design/icons'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../store/reducers'; +import { ExpandNavigationStackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +import './styles.less'; + +const BORDER_ITEMS = 2; +const NavigationStackShrinkedItem = () => { + const { links, shrinked, highlightIndex } = useSelector( + (state: RootState) => state.dataExplorer + ); + const dispatch = useDispatch(); + const count = links.length - BORDER_ITEMS; + const onClick = () => dispatch(ExpandNavigationStackDataExplorerGraphFlow()); + return ( +
+ Open more {count} other resources
} + > + + +
{count}
+ + {/*
+
*/} + + ); +}; + +export default NavigationStackShrinkedItem; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index bc9abbcc8..1fff9faa1 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1,3 +1,4 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; export { default as NavigationBack } from './NavigationBack'; export { default as NavigationStackItem } from './NavigationStackItem'; +export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; From cf3182e0fa43fc98331788f968c505b74a6cf2ca Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:32:30 +0200 Subject: [PATCH 004/192] de-10: add navigation humberger when nav-stack is expanded --- .../NavigationHamburguer.tsx | 24 +++++++++++++++++++ .../DataExplorerGraphFlowMolecules/index.ts | 1 + 2 files changed, 25 insertions(+) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx new file mode 100644 index 000000000..279168e21 --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { ShrinkOutlined } from '@ant-design/icons'; +import { RootState } from '../../store/reducers'; +import { + MAX_NAVIGATION_ITEMS_IN_STACK, + ShrinkNavigationStackDataExplorerGraphFlow, +} from '../../store/reducers/data-explorer'; +import './styles.less'; + +const NavigationHamburguer = () => { + const { shrinked, links } = useSelector( + (state: RootState) => state.dataExplorer + ); + const dispatch = useDispatch(); + const onShrink = () => dispatch(ShrinkNavigationStackDataExplorerGraphFlow()); + return !shrinked && links.length > MAX_NAVIGATION_ITEMS_IN_STACK ? ( + + ) : null; +}; + +export default NavigationHamburguer; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index 1fff9faa1..2dbf9d997 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -2,3 +2,4 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentL export { default as NavigationBack } from './NavigationBack'; export { default as NavigationStackItem } from './NavigationStackItem'; export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; +export { default as NavigationHamburguer } from './NavigationHamburguer'; From c52d6f0f0997cb972eaa285d08a587f11826e418 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:33:24 +0200 Subject: [PATCH 005/192] de-10: add navigation stack --- .../NavigationStack.tsx | 65 +++++++++++++++++++ .../styles.less | 17 +++++ 2 files changed, 82 insertions(+) create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx new file mode 100644 index 000000000..a0c89977e --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx @@ -0,0 +1,65 @@ +import React, { Fragment } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { clsx } from 'clsx'; +import { RootState } from '../../store/reducers'; +import { ResetHighlightedNodeDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +// import { +// NavigationHamburguer, +// NavigationStackItem, +// NavigationStackShrinkedItem, +// } from '../../molecules/DataExplorerGraphFlowMolecules'; +import './styles.less'; + +const NavigationStack = () => { + const dispatch = useDispatch(); + const { links, shrinked, highlightIndex } = useSelector( + (state: RootState) => state.dataExplorer + ); + + if (highlightIndex !== -1) { + setTimeout(() => { + dispatch(ResetHighlightedNodeDataExplorerGraphFlow()); + }, 2000); + } + return ( +
+ {/*
+ {links.map(({ title, types, _self, resource }, index) => { + if (index === 0) { + return ( + + + + + ); + } + return ( + + ); + })} +
*/} +
+ ); +}; + +export default NavigationStack; diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less new file mode 100644 index 000000000..e0e341915 --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less @@ -0,0 +1,17 @@ +.navigation-stack__wrapper { + display: flex; + align-items: flex-start; + position: fixed; + gap: 10px; + left: 0; + top: 52px; +} + +.navigation-stack { + grid-area: menu; + display: grid; + grid-auto-columns: max-content; + grid-auto-flow: column; + height: 100%; + min-height: 100vh; +} From 83e7191df04cbbb011a27b4a287be71edf2f0e87 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:37:15 +0200 Subject: [PATCH 006/192] de-10: integrate navigation stack to data explorer graph flow page --- .../DateExplorerGraphFlow.tsx | 74 ++++++++++++++++--- .../canvas/DataExplorerGraphFlow/styles.less | 47 +++++++++--- 2 files changed, 103 insertions(+), 18 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index 821e02287..b3128b220 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -1,15 +1,71 @@ -import React from 'react'; +import React, { useRef, useEffect, CSSProperties } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useLocation, useHistory } from 'react-router'; +import { clsx } from 'clsx'; +import { RootState } from '../../store/reducers'; +import { + DATA_EXPLORER_GRAPH_FLOW_DIGEST, + PopulateDataExplorerGraphFlow, +} from '../../store/reducers/data-explorer'; +import { + NavigationBack, + NavigationHamburguer, +} from '../../molecules/DataExplorerGraphFlowMolecules'; +import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; import './styles.less'; const DataExplorerResolverPage = () => { - return ( -
-
- -
-
- ); + const history = useHistory(); + const location = useLocation(); + const dispatch = useDispatch(); + const digestFirstRender = useRef(false); + const { links, shrinked } = useSelector( + (state: RootState) => state.dataExplorer + ); + + useEffect(() => { + if (!digestFirstRender.current) { + const state = localStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + if (state) { + dispatch(PopulateDataExplorerGraphFlow(state)); + } + } + digestFirstRender.current = true; + }, [location.search, digestFirstRender.current]); + + useEffect(() => { + const unlisten = history.listen(() => { + localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + }); + return () => unlisten(); + }, []); + + return ( +
+
+ +
+
+ + +
+
+ +
+
+ ); }; -export default DataExplorerResolverPage; +export default DataExplorerResolverPage; \ No newline at end of file diff --git a/src/shared/canvas/DataExplorerGraphFlow/styles.less b/src/shared/canvas/DataExplorerGraphFlow/styles.less index 43f675375..89b455a78 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/styles.less +++ b/src/shared/canvas/DataExplorerGraphFlow/styles.less @@ -1,12 +1,41 @@ @import '../..//lib.less'; .data-explorer-resolver { - width: 100%; - max-width: 100%; - align-items: flex-start; - justify-content: flex-start; - background-color: #f5f5f5; - display: grid; - grid-auto-columns: 1fr; - gap: 10px; -} + width: 100%; + max-width: 100%; + align-items: flex-start; + justify-content: flex-start; + background-color: #f5f5f5; + display: grid; + grid-auto-columns: 1fr; + gap: 10px; + + &.no-links { + grid-template-columns: 1fr; + } + + &.with-links { + grid-template-columns: calc(calc(var(--links-count) * 31px) + 40px) max-content 1fr; + } + + .degf__navigation-stack { + width: 100%; + } + + .degf__navigation-back { + position: sticky; + top: 53px; + display: flex; + align-items: center; + gap: 10px; + } + + .degf__content { + padding: 30px 20px 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + } +} \ No newline at end of file From 24801f11ddfaffc175a8045ba4641ece4162e182 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 03:01:04 +0200 Subject: [PATCH 007/192] de-10: add spring animtion --- .../NavigationStack.tsx | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx index a0c89977e..366e66b37 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx @@ -1,29 +1,49 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { clsx } from 'clsx'; import { RootState } from '../../store/reducers'; import { ResetHighlightedNodeDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; -// import { -// NavigationHamburguer, -// NavigationStackItem, -// NavigationStackShrinkedItem, -// } from '../../molecules/DataExplorerGraphFlowMolecules'; +import { + NavigationStackItem, + NavigationStackShrinkedItem, +} from '../../molecules/DataExplorerGraphFlowMolecules'; import './styles.less'; +import { animate, spring } from 'motion'; const NavigationStack = () => { const dispatch = useDispatch(); const { links, shrinked, highlightIndex } = useSelector( (state: RootState) => state.dataExplorer ); + const shrinkedRef = useRef(shrinked); + const stackRef = useRef(null); if (highlightIndex !== -1) { setTimeout(() => { dispatch(ResetHighlightedNodeDataExplorerGraphFlow()); }, 2000); } + useEffect(() => { + if (shrinkedRef.current && !shrinked && stackRef.current) { + animate( + stackRef.current, + { scale: 1 }, + { easing: spring({ stiffness: 10, damping: 10, mass: 500 }) } + ); + } else if (stackRef.current) { + animate( + stackRef.current, + { display: 'grid' }, + { easing: spring({ stiffness: 300, damping: 10 }) } + ); + } + }, [shrinked, shrinkedRef.current]); return (
- {/*
+
{links.map(({ title, types, _self, resource }, index) => { if (index === 0) { return ( @@ -57,7 +77,7 @@ const NavigationStack = () => { /> ); })} -
*/} +
); }; From ff2db8c4ed9162f9982eb3aec90f4307968daed7 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 03:01:39 +0200 Subject: [PATCH 008/192] de-10: fix editor popover click outside --- .../DateExplorerGraphFlow.tsx | 104 +++++++++--------- .../canvas/DataExplorerGraphFlow/styles.less | 66 +++++------ .../components/ResourceEditor/index.tsx | 2 - .../NavigationBack.tsx | 5 +- .../NavigationStackShrinkedItem.tsx | 2 - .../ResolvedLinkEditorPopover.tsx | 89 ++++++++------- src/shared/store/reducers/ui-settings.ts | 17 +-- 7 files changed, 142 insertions(+), 143 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index b3128b220..a86d343f1 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -4,68 +4,68 @@ import { useLocation, useHistory } from 'react-router'; import { clsx } from 'clsx'; import { RootState } from '../../store/reducers'; import { - DATA_EXPLORER_GRAPH_FLOW_DIGEST, - PopulateDataExplorerGraphFlow, + DATA_EXPLORER_GRAPH_FLOW_DIGEST, + PopulateDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; import { - NavigationBack, - NavigationHamburguer, + NavigationBack, + NavigationHamburguer, } from '../../molecules/DataExplorerGraphFlowMolecules'; import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; import './styles.less'; const DataExplorerResolverPage = () => { - const history = useHistory(); - const location = useLocation(); - const dispatch = useDispatch(); - const digestFirstRender = useRef(false); - const { links, shrinked } = useSelector( - (state: RootState) => state.dataExplorer - ); + const history = useHistory(); + const location = useLocation(); + const dispatch = useDispatch(); + const digestFirstRender = useRef(false); + const { links, shrinked } = useSelector( + (state: RootState) => state.dataExplorer + ); - useEffect(() => { - if (!digestFirstRender.current) { - const state = localStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); - if (state) { - dispatch(PopulateDataExplorerGraphFlow(state)); - } - } - digestFirstRender.current = true; - }, [location.search, digestFirstRender.current]); + useEffect(() => { + if (!digestFirstRender.current) { + const state = localStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + if (state) { + dispatch(PopulateDataExplorerGraphFlow(state)); + } + } + digestFirstRender.current = true; + }, [location.search, digestFirstRender.current]); - useEffect(() => { - const unlisten = history.listen(() => { - localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); - }); - return () => unlisten(); - }, []); + useEffect(() => { + const unlisten = history.listen(() => { + localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + }); + return () => unlisten(); + }, []); - return ( -
-
- -
-
- - -
-
- -
-
- ); + return ( +
+
+ +
+
+ + +
+
+ +
+
+ ); }; -export default DataExplorerResolverPage; \ No newline at end of file +export default DataExplorerResolverPage; diff --git a/src/shared/canvas/DataExplorerGraphFlow/styles.less b/src/shared/canvas/DataExplorerGraphFlow/styles.less index 89b455a78..f8c8a02e1 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/styles.less +++ b/src/shared/canvas/DataExplorerGraphFlow/styles.less @@ -1,41 +1,41 @@ @import '../..//lib.less'; .data-explorer-resolver { - width: 100%; - max-width: 100%; - align-items: flex-start; - justify-content: flex-start; - background-color: #f5f5f5; - display: grid; - grid-auto-columns: 1fr; - gap: 10px; + width: 100%; + max-width: 100%; + align-items: flex-start; + justify-content: flex-start; + background-color: #f5f5f5; + display: grid; + grid-auto-columns: 1fr; + gap: 10px; - &.no-links { - grid-template-columns: 1fr; - } + &.no-links { + grid-template-columns: 1fr; + } - &.with-links { - grid-template-columns: calc(calc(var(--links-count) * 31px) + 40px) max-content 1fr; - } + &.with-links { + grid-template-columns: calc(calc(var(--links-count) * 31px) + 40px) max-content 1fr; + } - .degf__navigation-stack { - width: 100%; - } + .degf__navigation-stack { + width: 100%; + } - .degf__navigation-back { - position: sticky; - top: 53px; - display: flex; - align-items: center; - gap: 10px; - } + .degf__navigation-back { + position: sticky; + top: 53px; + display: flex; + align-items: center; + gap: 10px; + } - .degf__content { - padding: 30px 20px 10px; - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: space-between; - gap: 10px; - } -} \ No newline at end of file + .degf__content { + padding: 30px 20px 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + } +} diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index d6f3d89bd..7fb0bb9d2 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -166,8 +166,6 @@ const ResourceEditor: React.FunctionComponent = props => { }); }; const onLinkClick = async (_: any, ev: MouseEvent) => { - // ev.preventDefault(); - ev.stopPropagation(); setLoadingResolution(true); const x = ev.pageX; const y = ev.pageY; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx index 7ae4f8ce3..86531a53e 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx @@ -11,10 +11,7 @@ const NavigationBack = () => { const onBack = () => dispatch(ReturnBackDataExplorerGraphFlow()); if (links.length) { return ( - diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx index b0a460ac3..746ea5c7a 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx @@ -32,8 +32,6 @@ const NavigationStackShrinkedItem = () => {
{count}
- {/*
-
*/} ); }; diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx index 8a8710fe5..6baabb70d 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx @@ -1,4 +1,11 @@ -import React, { ReactNode, useRef } from 'react'; +import React, { + ReactNode, + useEffect, + useRef, + useCallback, + forwardRef, + RefObject, +} from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory, useLocation, useRouteMatch } from 'react-router'; import { useNexusContext } from '@bbp/react-nexus'; @@ -13,46 +20,51 @@ import { InitNewVisitDataExplorerGraphView, AddNewNodeDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; -import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; +import { + TEditorPopoverResolvedData, + editorPopoverResolvedDataInitialValue, +} from '../../store/reducers/ui-settings'; import { getOrgAndProjectFromProjectId, getResourceLabel } from '../../utils'; import { getNormalizedTypes } from '../../components/ResourceEditor'; import useOnClickOutside from '../../hooks/useClickOutside'; import './styles.less'; type TResultPattern = Pick; - -const PopoverContainer = ({ - children, - onClickOutside, -}: { +type PopoverContainer = { children: ReactNode; onClickOutside(): void; -}) => { - const ref = useRef(null); - const { - editorPopoverResolvedData: { top, left, resolvedAs }, - } = useSelector((state: RootState) => state.uiSettings); - - useOnClickOutside(ref, onClickOutside); - return ( -
- {children} -
- ); }; +const PopoverContainer = forwardRef( + ({ children, onClickOutside }, ref) => { + const { + editorPopoverResolvedData: { top, left, resolvedAs }, + } = useSelector((state: RootState) => state.uiSettings); + + useOnClickOutside( + ref as React.MutableRefObject, + onClickOutside + ); + return ( +
+ {children} +
+ ); + } +); + const ResolvedLinkEditorPopover = () => { + const ref = useRef(null); const navigate = useHistory(); const dispatch = useDispatch(); const nexus = useNexusContext(); @@ -70,14 +82,7 @@ const ResolvedLinkEditorPopover = () => { const onClickOutside = () => { dispatch({ type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, - payload: { - open: false, - top: 0, - left: 0, - error: null, - results: [], - resolvedAs: undefined, - }, + payload: editorPopoverResolvedDataInitialValue, }); }; const onClickLink = async (resource: TDELink) => { @@ -112,14 +117,14 @@ const ResolvedLinkEditorPopover = () => { return pmatch(resultPattern) .with({ open: true, resolvedAs: 'error' }, () => ( - +
{error}
)) .with({ open: true, resolvedAs: 'resource' }, () => { const result = results as TDELink; return ( - +
{`${result.resource?.[0]}/${result.resource?.[1]}`} ) : null; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index 2dbf9d997..925d6bafb 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -2,4 +2,4 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentL export { default as NavigationBack } from './NavigationBack'; export { default as NavigationStackItem } from './NavigationStackItem'; export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; -export { default as NavigationHamburguer } from './NavigationHamburguer'; +export { default as NavigationCollapseButton } from './NavigationCollapseButton'; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index 7337fa51a..d4c78baf8 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -172,7 +172,7 @@ } } -.navigation-humburguer { +.navigation-collapse-btn { background: white; box-shadow: 0 2px 12px rgba(#333, 0.12); padding: 5px; From 5480678e7092ecfba2fd79c260937ddecdff3e0c Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 11:59:44 +0200 Subject: [PATCH 011/192] de-10: add hidden attribute to navigation stack item --- .../DataExplorerGraphFlowMolecules/NavigationStackItem.tsx | 1 + .../DataExplorerGraphFlowNavigationStack/NavigationStack.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx index 2b9f94fea..a1c327c4e 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -41,6 +41,7 @@ const NavigationStackItem = ({ highlighted && 'highlight', shrinked && index !== 0 && index !== links.length - 1 && 'shrinkable' )} + hidden={shrinked && index !== 0 && index !== links.length - 1} > { resource, }} /> - + ); } From 5c345bb33d5c055a6b63bb2b8e1e38dee13a2a45 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 11:59:59 +0200 Subject: [PATCH 012/192] de-10: add test for navigation stack --- .../NavigationStack.spec.tsx | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx new file mode 100644 index 000000000..6e2e5edf9 --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -0,0 +1,247 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, RenderResult, within } from '@testing-library/react'; + +import { Provider } from 'react-redux'; +import NavigationStack from './NavigationStack'; +import { createMemoryHistory } from 'history'; +import { createNexusClient } from '@bbp/nexus-sdk'; +import { deltaPath } from '__mocks__/handlers/handlers'; +import configureStore from '../../../shared/store'; +import { Router } from 'react-router-dom'; +import { NexusProvider } from '@bbp/react-nexus'; +import { + JumpToNodeDataExplorerGrpahFlow, + ReturnBackDataExplorerGraphFlow, + AddNewNodeDataExplorerGraphFlow, +} from '../../../shared/store/reducers/data-explorer'; +import { AnyAction, Store } from 'redux'; +import userEvent from '@testing-library/user-event'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; + +const fourthItemInStack = { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/datashapes:ontologyentity/https:%2F%2Fbbp.epfl.ch%2Fontologies%2Fcore%2Fefeatures%2FAHPAmplitude', + title: 'AHP Amplitude', + types: ['Class'], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://bbp.epfl.ch/ontologies/core/efeatures/AHPAmplitude', + 28, + ], +}; +describe('NavigationStack', () => { + let app: JSX.Element; + let component: RenderResult; + let container: HTMLElement; + let rerender: (ui: React.ReactElement) => void; + let store: Store; + let user: UserEvent; + beforeAll(() => { + const history = createMemoryHistory({}); + + const nexus = createNexusClient({ + fetch, + uri: deltaPath(), + }); + store = configureStore( + history, + { nexus }, + { + dataExplorer: { + current: { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.417881.3', + title: 'Allen Institute for Brain Science', + types: ['Agent', 'Organization'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.417881.3', + 1, + ], + }, + links: [ + { + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + title: '001_141216_A1_CA1py_R_MPG', + types: ['Entity', 'Trace', 'Dataset'], + resource: [ + 'bbp', + 'lnmce', + 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', + title: 'Ecole Polytechnique Federale de Lausanne', + types: ['Organization', 'prov#Agent'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.5333.6', + 1, + ], + }, + ], + shrinked: false, + highlightIndex: -1, + limited: false, + }, + } + ); + app = ( + + + + + + + + ); + component = render(app); + container = component.container; + rerender = component.rerender; + user = userEvent.setup(); + }); + + it('should render the correct number of NavigationStackItem components in the state', () => { + const navigationItems = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItems.length).toBe(2); + }); + it('should render the correct number of NavigationStackItem components after hit the return back btn', () => { + store.dispatch(ReturnBackDataExplorerGraphFlow()); + rerender(app); + const navigationItemsAfterBack = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItemsAfterBack.length).toBe(1); + }); + it('should render the correct number of NavigationStackItem after multiple navigation', () => { + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/_/https:%2F%2Fbbp.neuroshapes.org', + title: 'bbp.neuroshapes.org', + types: [], + resource: ['bbp', 'atlas', 'https://bbp.neuroshapes.org', 1], + }) + ); + rerender(app); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/_/https:%2F%2Fneuroshapes.org', + title: 'neuroshapes.org', + types: [], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://neuroshapes.org', + 161, + ], + }) + ); + render(app); + store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); + render(app); + const navigationItemsAfterMultipleNavigation = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItemsAfterMultipleNavigation.length).toBe(4); + const state = store.getState(); + expect(state.dataExplorer.links.length).toBe(4); + }); + it('should render the NavigationStackShrinkedItem when it passed MAX_NAVIGATION_ITEMS_IN_STACK', () => { + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/datashapes:ontologyentity/https:%2F%2Fbbp.epfl.ch%2Fontologies%2Fcore%2Fefeatures%2FNeuroElectroNeuronElectrophysiologicalFeature', + title: 'NeuroElectro Neuron Electrophysiological Feature', + types: ['Class'], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://bbp.epfl.ch/ontologies/core/efeatures/NeuroElectroNeuronElectrophysiologicalFeature', + 29, + ], + }) + ); + rerender(app); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:atlasrelease/4906ab85-694f-469d-962f-c0174e901885', + title: 'Blue Brain Atlas', + types: ['AtlasRelease', 'BrainAtlasRelease'], + resource: [ + 'bbp', + 'atlas', + 'https://bbp.epfl.ch/neurosciencegraph/data/4906ab85-694f-469d-962f-c0174e901885', + 3, + ], + }) + ); + rerender(app); + const navigationStackShrinkedItem = container.querySelectorAll( + '.navigation-stack-item.more' + ); + expect(navigationStackShrinkedItem.length).toBe(1); + expect(navigationStackShrinkedItem).not.toBeNull(); + const navigationHiddenItems = container.querySelectorAll( + '.navigation-stack-item:not(.no-more):not(.more)[hidden]' + ); + expect(navigationHiddenItems.length).toBe(4); + }); + it('should show the collapse button when the NavigationStackShrinkedItem is clicked', () => { + const navigationStackShrinkedItem = container.querySelector( + '.navigation-stack-item.more' + ); + if (navigationStackShrinkedItem) { + user.click(navigationStackShrinkedItem); + const collapseBtn = container.querySelector('.navigation-collapse-btn'); + expect(collapseBtn).not.toBeNull(); + expect(collapseBtn).toBeInTheDocument(); + expect(store.getState().dataExplorer.shrinked).toBe(false); + } + }); + it('should hide the collapse button when the collapse button is clicked', () => { + const navigationCollapseButton = container.querySelector( + '.navigation-collapse-btn' + ); + if (navigationCollapseButton) { + user.click(navigationCollapseButton); + const collapseBtn = container.querySelector('.navigation-collapse-btn'); + expect(collapseBtn).toBeNull(); + expect(store.getState().dataExplorer.shrinked).toBe(true); + } + }); + it('should the items in the stack be 4 when the user jump to the 5th item', () => { + store.dispatch(JumpToNodeDataExplorerGrpahFlow(4)); + rerender(app); + const navigationStackShrinkedItem = container.querySelector( + '.navigation-stack-item.more' + ); + expect(navigationStackShrinkedItem).toBeNull(); + const navigationItems = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItems.length).toBe(4); + }); + it('should the fourth item in the stack will be the current one when the user jump to the 4th item', () => { + const state = store.getState(); + expect(state.dataExplorer.current._self).toEqual(fourthItemInStack._self); + }); +}); From 54d0a3a1532e06117f9b5020053fa1f10437f6c2 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 12:57:39 +0200 Subject: [PATCH 013/192] de-10: add test for populating the dataexpolrer to store --- .../NavigationStack.spec.tsx | 107 ++++++++++-------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index 6e2e5edf9..6985c6174 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -14,11 +14,61 @@ import { JumpToNodeDataExplorerGrpahFlow, ReturnBackDataExplorerGraphFlow, AddNewNodeDataExplorerGraphFlow, + ResetDataExplorerGraphFlow, + PopulateDataExplorerGraphFlow, } from '../../../shared/store/reducers/data-explorer'; import { AnyAction, Store } from 'redux'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +const sampleDigest = + 'eyJjdXJyZW50Ijp7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6dm9sdW1ldHJpY2RhdGFsYXllci9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLCJ0aXRsZSI6IkJCUCBNb3VzZSBCcmFpbiBUZW1wbGF0ZSBWb2x1bWUsIDI1bSIsInR5cGVzIjpbIlZvbHVtZXRyaWNEYXRhTGF5ZXIiLCJCcmFpblRlbXBsYXRlRGF0YUxheWVyIiwiRGF0YXNldCJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLDJdfSwibGlua3MiOlt7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2xubWNlL2RhdGFzaGFwZXM6ZGF0YXNldC90cmFjZXMlMkY0NjBiZmEyZS1jYjdkLTQ0MjAtYTQ0OC0yMDMwYTZiZjRhZTQiLCJ0aXRsZSI6IjAwMV8xNDEyMTZfQTFfQ0ExcHlfUl9NUEciLCJ0eXBlcyI6WyJFbnRpdHkiLCJUcmFjZSIsIkRhdGFzZXQiXSwicmVzb3VyY2UiOlsiYmJwIiwibG5tY2UiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvdHJhY2VzLzQ2MGJmYTJlLWNiN2QtNDQyMC1hNDQ4LTIwMzBhNmJmNGFlNCIsMTVdfSx7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6YXRsYXNzcGF0aWFscmVmZXJlbmNlc3lzdGVtL2FsbGVuX2NjZnYzX3NwYXRpYWxfcmVmZXJlbmNlX3N5c3RlbSIsInRpdGxlIjoiQWxsZW4gTW91c2UgQ0NGIiwidHlwZXMiOlsiQXRsYXNTcGF0aWFsUmVmZXJlbmNlU3lzdGVtIiwiQnJhaW5BdGxhc1NwYXRpYWxSZWZlcmVuY2VTeXN0ZW0iXSwicmVzb3VyY2UiOlsiYmJwIiwiYXRsYXMiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvYWxsZW5fY2NmdjNfc3BhdGlhbF9yZWZlcmVuY2Vfc3lzdGVtIiw5XX1dLCJzaHJpbmtlZCI6ZmFsc2UsImhpZ2hsaWdodEluZGV4IjotMX0='; +const digestCurrentSelf = + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:volumetricdatalayer/cce06e96-85a4-403d-8b87-634a1554cdb9'; +const initialDataExplorerState = { + current: { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.417881.3', + title: 'Allen Institute for Brain Science', + types: ['Agent', 'Organization'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.417881.3', + 1, + ], + }, + links: [ + { + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + title: '001_141216_A1_CA1py_R_MPG', + types: ['Entity', 'Trace', 'Dataset'], + resource: [ + 'bbp', + 'lnmce', + 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', + title: 'Ecole Polytechnique Federale de Lausanne', + types: ['Organization', 'prov#Agent'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.5333.6', + 1, + ], + }, + ], + shrinked: false, + highlightIndex: -1, + limited: false, +}; const fourthItemInStack = { isDownloadable: false, _self: @@ -49,52 +99,7 @@ describe('NavigationStack', () => { store = configureStore( history, { nexus }, - { - dataExplorer: { - current: { - isDownloadable: false, - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.417881.3', - title: 'Allen Institute for Brain Science', - types: ['Agent', 'Organization'], - resource: [ - 'bbp', - 'atlas', - 'https://www.grid.ac/institutes/grid.417881.3', - 1, - ], - }, - links: [ - { - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', - title: '001_141216_A1_CA1py_R_MPG', - types: ['Entity', 'Trace', 'Dataset'], - resource: [ - 'bbp', - 'lnmce', - 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', - ], - }, - { - isDownloadable: false, - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', - title: 'Ecole Polytechnique Federale de Lausanne', - types: ['Organization', 'prov#Agent'], - resource: [ - 'bbp', - 'atlas', - 'https://www.grid.ac/institutes/grid.5333.6', - 1, - ], - }, - ], - shrinked: false, - highlightIndex: -1, - limited: false, - }, - } + { dataExplorer: initialDataExplorerState } ); app = ( @@ -110,7 +115,6 @@ describe('NavigationStack', () => { rerender = component.rerender; user = userEvent.setup(); }); - it('should render the correct number of NavigationStackItem components in the state', () => { const navigationItems = container.querySelectorAll( '.navigation-stack-item:not(.no-more)' @@ -244,4 +248,13 @@ describe('NavigationStack', () => { const state = store.getState(); expect(state.dataExplorer.current._self).toEqual(fourthItemInStack._self); }); + it('should decode the navigation digest at first render', () => { + store.dispatch(ResetDataExplorerGraphFlow()); + store.dispatch(PopulateDataExplorerGraphFlow(sampleDigest)); + render(app); + const dataExplorerState = store.getState().dataExplorer; + expect(dataExplorerState.links.length).toBe(2); + expect(dataExplorerState.current._self).toBe(digestCurrentSelf); + expect(dataExplorerState.shrinked).toBe(false); + }); }); From b693bdaf944a4855b46b8d2b7dfa914432c1be2f Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 12:57:55 +0200 Subject: [PATCH 014/192] de-10: reset data explorer state when quiting the page --- .../canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index f98ef5840..b1423afc8 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -6,6 +6,7 @@ import { RootState } from '../../store/reducers'; import { DATA_EXPLORER_GRAPH_FLOW_DIGEST, PopulateDataExplorerGraphFlow, + ResetDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; import { NavigationBack, @@ -36,6 +37,7 @@ const DataExplorerResolverPage = () => { useEffect(() => { const unlisten = history.listen(() => { + dispatch(ResetDataExplorerGraphFlow()); localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); }); return () => unlisten(); From 9863c8cd31a872b2de9f516b71d6e7c427aa039f Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:29:48 +0200 Subject: [PATCH 015/192] de-10: add navigation back button --- .../NavigationBack.tsx | 26 +++++++++++++++++++ .../DataExplorerGraphFlowMolecules/index.ts | 1 + 2 files changed, 27 insertions(+) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx new file mode 100644 index 000000000..7ae4f8ce3 --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { ReturnBackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +import { RootState } from '../../store/reducers'; +import './styles.less'; + +const NavigationBack = () => { + const dispatch = useDispatch(); + const { links } = useSelector((state: RootState) => state.dataExplorer); + const onBack = () => dispatch(ReturnBackDataExplorerGraphFlow()); + if (links.length) { + return ( + + ); + } + return null; +}; + +export default NavigationBack; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index 41a847ac9..ffb1961ed 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1 +1,2 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; +export { default as NavigationBack } from './NavigationBack'; From 4b84ed8dbee7a973a9f47717368c12f42c757442 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:30:43 +0200 Subject: [PATCH 016/192] de-10: add navigation stack item --- .../NavigationStackItem.tsx | 66 +++++++++++++++++++ .../DataExplorerGraphFlowMolecules/index.ts | 1 + 2 files changed, 67 insertions(+) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx new file mode 100644 index 000000000..2b9f94fea --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Tag, Tooltip } from 'antd'; +import { clsx } from 'clsx'; +import { isArray } from 'lodash'; +import { PlusOutlined } from '@ant-design/icons'; +import { + TDEResource, + JumpToNodeDataExplorerGrpahFlow, +} from '../../store/reducers/data-explorer'; +import { RootState } from '../../store/reducers'; +import './styles.less'; + +export type TNavigationStackItem = { + _self: string; + index: number; + types?: string | string[]; + title: string; + resource?: TDEResource; + highlighted: boolean; +}; + +const NavigationStackItem = ({ + index, + _self, + title, + types, + resource, + highlighted, +}: TNavigationStackItem) => { + const dispatch = useDispatch(); + const { shrinked, links } = useSelector( + (state: RootState) => state.dataExplorer + ); + const onClick = () => dispatch(JumpToNodeDataExplorerGrpahFlow(index)); + + return ( +
+ + {`${resource?.[0]}/${resource?.[1]}`} + {decodeURIComponent(_self)} +
+ } + > + +
+ {`${resource?.[0]}/${resource?.[1]}`} +
{title}
+ {types && ( +
{isArray(types) ? types.join(', ') : types}
+ )} +
+ ); +}; + +export default NavigationStackItem; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index ffb1961ed..bc9abbcc8 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1,2 +1,3 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; export { default as NavigationBack } from './NavigationBack'; +export { default as NavigationStackItem } from './NavigationStackItem'; From 57569c64708d42eae30db7b218a8b216be140ca4 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:31:36 +0200 Subject: [PATCH 017/192] de-10: add navigation stack shrinked item --- .../NavigationStackShrinkedItem.tsx | 41 +++++++++++++++++++ .../DataExplorerGraphFlowMolecules/index.ts | 1 + 2 files changed, 42 insertions(+) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx new file mode 100644 index 000000000..b0a460ac3 --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Tooltip } from 'antd'; +import { clsx } from 'clsx'; +import { MoreOutlined, PlusOutlined } from '@ant-design/icons'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../store/reducers'; +import { ExpandNavigationStackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +import './styles.less'; + +const BORDER_ITEMS = 2; +const NavigationStackShrinkedItem = () => { + const { links, shrinked, highlightIndex } = useSelector( + (state: RootState) => state.dataExplorer + ); + const dispatch = useDispatch(); + const count = links.length - BORDER_ITEMS; + const onClick = () => dispatch(ExpandNavigationStackDataExplorerGraphFlow()); + return ( +
+ Open more {count} other resources
} + > + + +
{count}
+ + {/*
+
*/} + + ); +}; + +export default NavigationStackShrinkedItem; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index bc9abbcc8..1fff9faa1 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1,3 +1,4 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; export { default as NavigationBack } from './NavigationBack'; export { default as NavigationStackItem } from './NavigationStackItem'; +export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; From c792c89ad4d99ab39999f10f3ab612fbe0f4a93f Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:32:30 +0200 Subject: [PATCH 018/192] de-10: add navigation humberger when nav-stack is expanded --- .../NavigationHamburguer.tsx | 24 +++++++++++++++++++ .../DataExplorerGraphFlowMolecules/index.ts | 1 + 2 files changed, 25 insertions(+) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx new file mode 100644 index 000000000..279168e21 --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { ShrinkOutlined } from '@ant-design/icons'; +import { RootState } from '../../store/reducers'; +import { + MAX_NAVIGATION_ITEMS_IN_STACK, + ShrinkNavigationStackDataExplorerGraphFlow, +} from '../../store/reducers/data-explorer'; +import './styles.less'; + +const NavigationHamburguer = () => { + const { shrinked, links } = useSelector( + (state: RootState) => state.dataExplorer + ); + const dispatch = useDispatch(); + const onShrink = () => dispatch(ShrinkNavigationStackDataExplorerGraphFlow()); + return !shrinked && links.length > MAX_NAVIGATION_ITEMS_IN_STACK ? ( + + ) : null; +}; + +export default NavigationHamburguer; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index 1fff9faa1..2dbf9d997 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -2,3 +2,4 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentL export { default as NavigationBack } from './NavigationBack'; export { default as NavigationStackItem } from './NavigationStackItem'; export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; +export { default as NavigationHamburguer } from './NavigationHamburguer'; From 3b6d9d66498a92abbf000fcecdd29950e4236bf8 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:33:24 +0200 Subject: [PATCH 019/192] de-10: add navigation stack --- .../NavigationStack.tsx | 65 +++++++++++++++++++ .../styles.less | 17 +++++ 2 files changed, 82 insertions(+) create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx new file mode 100644 index 000000000..a0c89977e --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx @@ -0,0 +1,65 @@ +import React, { Fragment } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { clsx } from 'clsx'; +import { RootState } from '../../store/reducers'; +import { ResetHighlightedNodeDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +// import { +// NavigationHamburguer, +// NavigationStackItem, +// NavigationStackShrinkedItem, +// } from '../../molecules/DataExplorerGraphFlowMolecules'; +import './styles.less'; + +const NavigationStack = () => { + const dispatch = useDispatch(); + const { links, shrinked, highlightIndex } = useSelector( + (state: RootState) => state.dataExplorer + ); + + if (highlightIndex !== -1) { + setTimeout(() => { + dispatch(ResetHighlightedNodeDataExplorerGraphFlow()); + }, 2000); + } + return ( +
+ {/*
+ {links.map(({ title, types, _self, resource }, index) => { + if (index === 0) { + return ( + + + + + ); + } + return ( + + ); + })} +
*/} +
+ ); +}; + +export default NavigationStack; diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less new file mode 100644 index 000000000..e0e341915 --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less @@ -0,0 +1,17 @@ +.navigation-stack__wrapper { + display: flex; + align-items: flex-start; + position: fixed; + gap: 10px; + left: 0; + top: 52px; +} + +.navigation-stack { + grid-area: menu; + display: grid; + grid-auto-columns: max-content; + grid-auto-flow: column; + height: 100%; + min-height: 100vh; +} From 46026795acac012ac7f32f6f0d887376c8b6b375 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 01:37:15 +0200 Subject: [PATCH 020/192] de-10: integrate navigation stack to data explorer graph flow page --- .../DateExplorerGraphFlow.tsx | 60 ++++++++++++++++++- .../canvas/DataExplorerGraphFlow/styles.less | 29 +++++++++ .../NavigationBack.tsx | 5 +- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index 821e02287..a86d343f1 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -1,10 +1,66 @@ -import React from 'react'; +import React, { useRef, useEffect, CSSProperties } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useLocation, useHistory } from 'react-router'; +import { clsx } from 'clsx'; +import { RootState } from '../../store/reducers'; +import { + DATA_EXPLORER_GRAPH_FLOW_DIGEST, + PopulateDataExplorerGraphFlow, +} from '../../store/reducers/data-explorer'; +import { + NavigationBack, + NavigationHamburguer, +} from '../../molecules/DataExplorerGraphFlowMolecules'; +import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; import './styles.less'; const DataExplorerResolverPage = () => { + const history = useHistory(); + const location = useLocation(); + const dispatch = useDispatch(); + const digestFirstRender = useRef(false); + const { links, shrinked } = useSelector( + (state: RootState) => state.dataExplorer + ); + + useEffect(() => { + if (!digestFirstRender.current) { + const state = localStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + if (state) { + dispatch(PopulateDataExplorerGraphFlow(state)); + } + } + digestFirstRender.current = true; + }, [location.search, digestFirstRender.current]); + + useEffect(() => { + const unlisten = history.listen(() => { + localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + }); + return () => unlisten(); + }, []); + return ( -
+
+
+ +
+
+ + +
diff --git a/src/shared/canvas/DataExplorerGraphFlow/styles.less b/src/shared/canvas/DataExplorerGraphFlow/styles.less index 2a51a48e9..50a410c27 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/styles.less +++ b/src/shared/canvas/DataExplorerGraphFlow/styles.less @@ -9,4 +9,33 @@ display: grid; grid-auto-columns: 1fr; gap: 10px; + + &.no-links { + grid-template-columns: 1fr; + } + + &.with-links { + grid-template-columns: calc(calc(var(--links-count) * 31px) + 40px) max-content 1fr; + } + + .degf__navigation-stack { + width: 100%; + } + + .degf__navigation-back { + position: sticky; + top: 53px; + display: flex; + align-items: center; + gap: 10px; + } + + .degf__content { + padding: 30px 20px 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + } } diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx index 7ae4f8ce3..86531a53e 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx @@ -11,10 +11,7 @@ const NavigationBack = () => { const onBack = () => dispatch(ReturnBackDataExplorerGraphFlow()); if (links.length) { return ( - From b089ac44d337f796b7d57ea396b19a922356876c Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 03:01:04 +0200 Subject: [PATCH 021/192] de-10: add spring animtion --- .../NavigationStack.tsx | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx index a0c89977e..366e66b37 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx @@ -1,29 +1,49 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { clsx } from 'clsx'; import { RootState } from '../../store/reducers'; import { ResetHighlightedNodeDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; -// import { -// NavigationHamburguer, -// NavigationStackItem, -// NavigationStackShrinkedItem, -// } from '../../molecules/DataExplorerGraphFlowMolecules'; +import { + NavigationStackItem, + NavigationStackShrinkedItem, +} from '../../molecules/DataExplorerGraphFlowMolecules'; import './styles.less'; +import { animate, spring } from 'motion'; const NavigationStack = () => { const dispatch = useDispatch(); const { links, shrinked, highlightIndex } = useSelector( (state: RootState) => state.dataExplorer ); + const shrinkedRef = useRef(shrinked); + const stackRef = useRef(null); if (highlightIndex !== -1) { setTimeout(() => { dispatch(ResetHighlightedNodeDataExplorerGraphFlow()); }, 2000); } + useEffect(() => { + if (shrinkedRef.current && !shrinked && stackRef.current) { + animate( + stackRef.current, + { scale: 1 }, + { easing: spring({ stiffness: 10, damping: 10, mass: 500 }) } + ); + } else if (stackRef.current) { + animate( + stackRef.current, + { display: 'grid' }, + { easing: spring({ stiffness: 300, damping: 10 }) } + ); + } + }, [shrinked, shrinkedRef.current]); return (
- {/*
+
{links.map(({ title, types, _self, resource }, index) => { if (index === 0) { return ( @@ -57,7 +77,7 @@ const NavigationStack = () => { /> ); })} -
*/} +
); }; From e7b505a3affa82b0858af109cb0f147297de51b5 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 03:01:39 +0200 Subject: [PATCH 022/192] de-10: fix editor popover click outside --- .../NavigationStackShrinkedItem.tsx | 2 - .../ResolvedLinkEditorPopover.tsx | 76 +++++++++---------- src/shared/store/reducers/ui-settings.ts | 17 +++-- 3 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx index b0a460ac3..746ea5c7a 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx @@ -32,8 +32,6 @@ const NavigationStackShrinkedItem = () => {
{count}
- {/*
-
*/}
); }; diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx index 5c1e430a2..704b0776a 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useRef } from 'react'; +import React, { ReactNode, useRef, forwardRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory, useLocation, useRouteMatch } from 'react-router'; import { useNexusContext } from '@bbp/react-nexus'; @@ -13,43 +13,48 @@ import { InitNewVisitDataExplorerGraphView, AddNewNodeDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; -import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; +import { + TEditorPopoverResolvedData, + editorPopoverResolvedDataInitialValue, +} from '../../store/reducers/ui-settings'; import { getOrgAndProjectFromProjectId, getResourceLabel } from '../../utils'; import { getNormalizedTypes } from '../../components/ResourceEditor/editorUtils'; import useOnClickOutside from '../../hooks/useClickOutside'; import './styles.less'; type TResultPattern = Pick; - -const PopoverContainer = ({ - children, - onClickOutside, -}: { +type PopoverContainer = { children: ReactNode; onClickOutside(): void; -}) => { - const ref = useRef(null); - const { - editorPopoverResolvedData: { top, left, resolvedAs }, - } = useSelector((state: RootState) => state.uiSettings); - - useOnClickOutside(ref, onClickOutside); - return ( -
- {children} -
- ); }; +const PopoverContainer = forwardRef( + ({ children, onClickOutside }, ref) => { + const { + editorPopoverResolvedData: { top, left, resolvedAs }, + } = useSelector((state: RootState) => state.uiSettings); + + useOnClickOutside( + ref as React.MutableRefObject, + onClickOutside + ); + return ( +
+ {children} +
+ ); + } +); + const ResolvedLinkEditorPopover = () => { + const ref = useRef(null); const navigate = useHistory(); const dispatch = useDispatch(); const nexus = useNexusContext(); @@ -67,14 +72,7 @@ const ResolvedLinkEditorPopover = () => { const onClickOutside = () => { dispatch({ type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, - payload: { - open: false, - top: 0, - left: 0, - error: null, - results: [], - resolvedAs: undefined, - }, + payload: editorPopoverResolvedDataInitialValue, }); }; const onClickLink = async (resource: TDELink) => { @@ -109,14 +107,14 @@ const ResolvedLinkEditorPopover = () => { return pmatch(resultPattern) .with({ open: true, resolvedAs: 'error' }, () => ( - +
{error}
)) .with({ open: true, resolvedAs: 'resource' }, () => { const result = results as TDELink; return ( - +
{result.resource?.[0] && result.resource?.[1] && ( {`${result.resource?.[0]}/${result.resource?.[1]}`} @@ -133,7 +131,7 @@ const ResolvedLinkEditorPopover = () => { }) .with({ open: true, resolvedAs: 'resources' }, () => { return ( - + {(results as TDELink[]).map(item => (
{item.resource?.[0] && item.resource?.[1] && ( @@ -153,7 +151,7 @@ const ResolvedLinkEditorPopover = () => { .with({ open: true, resolvedAs: 'external' }, () => { const result = results as TDELink; return ( - +
External Link diff --git a/src/shared/store/reducers/ui-settings.ts b/src/shared/store/reducers/ui-settings.ts index e847e7cc1..963a5d3cb 100644 --- a/src/shared/store/reducers/ui-settings.ts +++ b/src/shared/store/reducers/ui-settings.ts @@ -6,6 +6,14 @@ import { } from '../actions/ui-settings'; import { TDELink } from './data-explorer'; +export const editorPopoverResolvedDataInitialValue = { + top: 0, + left: 0, + open: false, + results: [], + error: null, + resolvedAs: undefined, +}; export const DEFAULT_UI_SETTINGS: UISettingsState = { openCreationPanel: false, pageSizes: { @@ -15,14 +23,7 @@ export const DEFAULT_UI_SETTINGS: UISettingsState = { linksListPageSize: 10, }, currentResourceView: null, - editorPopoverResolvedData: { - top: 0, - left: 0, - open: false, - results: [], - error: null, - resolvedAs: undefined, - }, + editorPopoverResolvedData: editorPopoverResolvedDataInitialValue, }; export type TEditorPopoverResolvedAs = | 'resource' From 99244fe04a11717f26884d294d37d769433383e2 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 03:17:14 +0200 Subject: [PATCH 023/192] de-10: add some animation to already stacked item when resolving --- .../styles.less | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index b84039a78..03bc66bf5 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -24,8 +24,13 @@ } &.highlight { - border: 2px solid #377af5; + background: rgba(#377af5, 0.12); + border: 1.5px solid #377af5; + border-top: none; + border-bottom: none; transition: border-color 0s ease-out infinite; + -webkit-animation: vibrate 0.3s linear infinite both; + animation: vibrate 0.3s linear infinite both; } &.shrinkable { @@ -178,3 +183,30 @@ border: 1px solid #afacacd8; cursor: pointer; } + +@keyframes vibrate { + 0% { + -webkit-transform: translate(0); + transform: translate(0); + } + 20% { + -webkit-transform: translate(-1.5px, 1.5px); + transform: translate(-1.5px, 1.5px); + } + 40% { + -webkit-transform: translate(-1.5px, -1.5px); + transform: translate(-1.5px, -1.5px); + } + 60% { + -webkit-transform: translate(1.5px, 1.5px); + transform: translate(1.5px, 1.5px); + } + 80% { + -webkit-transform: translate(1.5px, -1.5px); + transform: translate(1.5px, -1.5px); + } + 100% { + -webkit-transform: translate(0); + transform: translate(0); + } +} From e532c8842605a8c57dbf8ce9deed16c97dad85b3 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 11:57:53 +0200 Subject: [PATCH 024/192] de-10: rename the navigation collapse btn and classname --- .../canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx | 4 ++-- ...{NavigationHamburguer.tsx => NavigationCollapseButton.tsx} | 2 +- src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts | 2 +- .../molecules/DataExplorerGraphFlowMolecules/styles.less | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/shared/molecules/DataExplorerGraphFlowMolecules/{NavigationHamburguer.tsx => NavigationCollapseButton.tsx} (91%) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index a86d343f1..f98ef5840 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -9,7 +9,7 @@ import { } from '../../store/reducers/data-explorer'; import { NavigationBack, - NavigationHamburguer, + NavigationCollapseButton, } from '../../molecules/DataExplorerGraphFlowMolecules'; import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; @@ -58,7 +58,7 @@ const DataExplorerResolverPage = () => {
- +
diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx similarity index 91% rename from src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx rename to src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx index 279168e21..3fe021786 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationHamburguer.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx @@ -15,7 +15,7 @@ const NavigationHamburguer = () => { const dispatch = useDispatch(); const onShrink = () => dispatch(ShrinkNavigationStackDataExplorerGraphFlow()); return !shrinked && links.length > MAX_NAVIGATION_ITEMS_IN_STACK ? ( - ) : null; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index 2dbf9d997..925d6bafb 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -2,4 +2,4 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentL export { default as NavigationBack } from './NavigationBack'; export { default as NavigationStackItem } from './NavigationStackItem'; export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; -export { default as NavigationHamburguer } from './NavigationHamburguer'; +export { default as NavigationCollapseButton } from './NavigationCollapseButton'; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index 03bc66bf5..e6429a520 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -172,7 +172,7 @@ } } -.navigation-humburguer { +.navigation-collapse-btn { background: white; box-shadow: 0 2px 12px rgba(#333, 0.12); padding: 5px; From fbaedef3907389af49142c2fc7994e3b4d49b2d0 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 11:59:44 +0200 Subject: [PATCH 025/192] de-10: add hidden attribute to navigation stack item --- .../DataExplorerGraphFlowMolecules/NavigationStackItem.tsx | 1 + .../DataExplorerGraphFlowNavigationStack/NavigationStack.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx index 2b9f94fea..a1c327c4e 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -41,6 +41,7 @@ const NavigationStackItem = ({ highlighted && 'highlight', shrinked && index !== 0 && index !== links.length - 1 && 'shrinkable' )} + hidden={shrinked && index !== 0 && index !== links.length - 1} > { resource, }} /> - + ); } From 191ac25ff07c1e0025da48702b5dd2c83ed0159a Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 11:59:59 +0200 Subject: [PATCH 026/192] de-10: add test for navigation stack --- .../NavigationStack.spec.tsx | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx new file mode 100644 index 000000000..6e2e5edf9 --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -0,0 +1,247 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, RenderResult, within } from '@testing-library/react'; + +import { Provider } from 'react-redux'; +import NavigationStack from './NavigationStack'; +import { createMemoryHistory } from 'history'; +import { createNexusClient } from '@bbp/nexus-sdk'; +import { deltaPath } from '__mocks__/handlers/handlers'; +import configureStore from '../../../shared/store'; +import { Router } from 'react-router-dom'; +import { NexusProvider } from '@bbp/react-nexus'; +import { + JumpToNodeDataExplorerGrpahFlow, + ReturnBackDataExplorerGraphFlow, + AddNewNodeDataExplorerGraphFlow, +} from '../../../shared/store/reducers/data-explorer'; +import { AnyAction, Store } from 'redux'; +import userEvent from '@testing-library/user-event'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; + +const fourthItemInStack = { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/datashapes:ontologyentity/https:%2F%2Fbbp.epfl.ch%2Fontologies%2Fcore%2Fefeatures%2FAHPAmplitude', + title: 'AHP Amplitude', + types: ['Class'], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://bbp.epfl.ch/ontologies/core/efeatures/AHPAmplitude', + 28, + ], +}; +describe('NavigationStack', () => { + let app: JSX.Element; + let component: RenderResult; + let container: HTMLElement; + let rerender: (ui: React.ReactElement) => void; + let store: Store; + let user: UserEvent; + beforeAll(() => { + const history = createMemoryHistory({}); + + const nexus = createNexusClient({ + fetch, + uri: deltaPath(), + }); + store = configureStore( + history, + { nexus }, + { + dataExplorer: { + current: { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.417881.3', + title: 'Allen Institute for Brain Science', + types: ['Agent', 'Organization'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.417881.3', + 1, + ], + }, + links: [ + { + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + title: '001_141216_A1_CA1py_R_MPG', + types: ['Entity', 'Trace', 'Dataset'], + resource: [ + 'bbp', + 'lnmce', + 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', + title: 'Ecole Polytechnique Federale de Lausanne', + types: ['Organization', 'prov#Agent'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.5333.6', + 1, + ], + }, + ], + shrinked: false, + highlightIndex: -1, + limited: false, + }, + } + ); + app = ( + + + + + + + + ); + component = render(app); + container = component.container; + rerender = component.rerender; + user = userEvent.setup(); + }); + + it('should render the correct number of NavigationStackItem components in the state', () => { + const navigationItems = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItems.length).toBe(2); + }); + it('should render the correct number of NavigationStackItem components after hit the return back btn', () => { + store.dispatch(ReturnBackDataExplorerGraphFlow()); + rerender(app); + const navigationItemsAfterBack = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItemsAfterBack.length).toBe(1); + }); + it('should render the correct number of NavigationStackItem after multiple navigation', () => { + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/_/https:%2F%2Fbbp.neuroshapes.org', + title: 'bbp.neuroshapes.org', + types: [], + resource: ['bbp', 'atlas', 'https://bbp.neuroshapes.org', 1], + }) + ); + rerender(app); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/_/https:%2F%2Fneuroshapes.org', + title: 'neuroshapes.org', + types: [], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://neuroshapes.org', + 161, + ], + }) + ); + render(app); + store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); + render(app); + const navigationItemsAfterMultipleNavigation = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItemsAfterMultipleNavigation.length).toBe(4); + const state = store.getState(); + expect(state.dataExplorer.links.length).toBe(4); + }); + it('should render the NavigationStackShrinkedItem when it passed MAX_NAVIGATION_ITEMS_IN_STACK', () => { + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/datashapes:ontologyentity/https:%2F%2Fbbp.epfl.ch%2Fontologies%2Fcore%2Fefeatures%2FNeuroElectroNeuronElectrophysiologicalFeature', + title: 'NeuroElectro Neuron Electrophysiological Feature', + types: ['Class'], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://bbp.epfl.ch/ontologies/core/efeatures/NeuroElectroNeuronElectrophysiologicalFeature', + 29, + ], + }) + ); + rerender(app); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:atlasrelease/4906ab85-694f-469d-962f-c0174e901885', + title: 'Blue Brain Atlas', + types: ['AtlasRelease', 'BrainAtlasRelease'], + resource: [ + 'bbp', + 'atlas', + 'https://bbp.epfl.ch/neurosciencegraph/data/4906ab85-694f-469d-962f-c0174e901885', + 3, + ], + }) + ); + rerender(app); + const navigationStackShrinkedItem = container.querySelectorAll( + '.navigation-stack-item.more' + ); + expect(navigationStackShrinkedItem.length).toBe(1); + expect(navigationStackShrinkedItem).not.toBeNull(); + const navigationHiddenItems = container.querySelectorAll( + '.navigation-stack-item:not(.no-more):not(.more)[hidden]' + ); + expect(navigationHiddenItems.length).toBe(4); + }); + it('should show the collapse button when the NavigationStackShrinkedItem is clicked', () => { + const navigationStackShrinkedItem = container.querySelector( + '.navigation-stack-item.more' + ); + if (navigationStackShrinkedItem) { + user.click(navigationStackShrinkedItem); + const collapseBtn = container.querySelector('.navigation-collapse-btn'); + expect(collapseBtn).not.toBeNull(); + expect(collapseBtn).toBeInTheDocument(); + expect(store.getState().dataExplorer.shrinked).toBe(false); + } + }); + it('should hide the collapse button when the collapse button is clicked', () => { + const navigationCollapseButton = container.querySelector( + '.navigation-collapse-btn' + ); + if (navigationCollapseButton) { + user.click(navigationCollapseButton); + const collapseBtn = container.querySelector('.navigation-collapse-btn'); + expect(collapseBtn).toBeNull(); + expect(store.getState().dataExplorer.shrinked).toBe(true); + } + }); + it('should the items in the stack be 4 when the user jump to the 5th item', () => { + store.dispatch(JumpToNodeDataExplorerGrpahFlow(4)); + rerender(app); + const navigationStackShrinkedItem = container.querySelector( + '.navigation-stack-item.more' + ); + expect(navigationStackShrinkedItem).toBeNull(); + const navigationItems = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItems.length).toBe(4); + }); + it('should the fourth item in the stack will be the current one when the user jump to the 4th item', () => { + const state = store.getState(); + expect(state.dataExplorer.current._self).toEqual(fourthItemInStack._self); + }); +}); From 2fcab82fa5e573e601e1f9d12a440dfc437a3b0e Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 12:57:39 +0200 Subject: [PATCH 027/192] de-10: add test for populating the dataexpolrer to store --- .../NavigationStack.spec.tsx | 107 ++++++++++-------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index 6e2e5edf9..6985c6174 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -14,11 +14,61 @@ import { JumpToNodeDataExplorerGrpahFlow, ReturnBackDataExplorerGraphFlow, AddNewNodeDataExplorerGraphFlow, + ResetDataExplorerGraphFlow, + PopulateDataExplorerGraphFlow, } from '../../../shared/store/reducers/data-explorer'; import { AnyAction, Store } from 'redux'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +const sampleDigest = + 'eyJjdXJyZW50Ijp7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6dm9sdW1ldHJpY2RhdGFsYXllci9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLCJ0aXRsZSI6IkJCUCBNb3VzZSBCcmFpbiBUZW1wbGF0ZSBWb2x1bWUsIDI1bSIsInR5cGVzIjpbIlZvbHVtZXRyaWNEYXRhTGF5ZXIiLCJCcmFpblRlbXBsYXRlRGF0YUxheWVyIiwiRGF0YXNldCJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLDJdfSwibGlua3MiOlt7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2xubWNlL2RhdGFzaGFwZXM6ZGF0YXNldC90cmFjZXMlMkY0NjBiZmEyZS1jYjdkLTQ0MjAtYTQ0OC0yMDMwYTZiZjRhZTQiLCJ0aXRsZSI6IjAwMV8xNDEyMTZfQTFfQ0ExcHlfUl9NUEciLCJ0eXBlcyI6WyJFbnRpdHkiLCJUcmFjZSIsIkRhdGFzZXQiXSwicmVzb3VyY2UiOlsiYmJwIiwibG5tY2UiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvdHJhY2VzLzQ2MGJmYTJlLWNiN2QtNDQyMC1hNDQ4LTIwMzBhNmJmNGFlNCIsMTVdfSx7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6YXRsYXNzcGF0aWFscmVmZXJlbmNlc3lzdGVtL2FsbGVuX2NjZnYzX3NwYXRpYWxfcmVmZXJlbmNlX3N5c3RlbSIsInRpdGxlIjoiQWxsZW4gTW91c2UgQ0NGIiwidHlwZXMiOlsiQXRsYXNTcGF0aWFsUmVmZXJlbmNlU3lzdGVtIiwiQnJhaW5BdGxhc1NwYXRpYWxSZWZlcmVuY2VTeXN0ZW0iXSwicmVzb3VyY2UiOlsiYmJwIiwiYXRsYXMiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvYWxsZW5fY2NmdjNfc3BhdGlhbF9yZWZlcmVuY2Vfc3lzdGVtIiw5XX1dLCJzaHJpbmtlZCI6ZmFsc2UsImhpZ2hsaWdodEluZGV4IjotMX0='; +const digestCurrentSelf = + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:volumetricdatalayer/cce06e96-85a4-403d-8b87-634a1554cdb9'; +const initialDataExplorerState = { + current: { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.417881.3', + title: 'Allen Institute for Brain Science', + types: ['Agent', 'Organization'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.417881.3', + 1, + ], + }, + links: [ + { + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + title: '001_141216_A1_CA1py_R_MPG', + types: ['Entity', 'Trace', 'Dataset'], + resource: [ + 'bbp', + 'lnmce', + 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', + title: 'Ecole Polytechnique Federale de Lausanne', + types: ['Organization', 'prov#Agent'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.5333.6', + 1, + ], + }, + ], + shrinked: false, + highlightIndex: -1, + limited: false, +}; const fourthItemInStack = { isDownloadable: false, _self: @@ -49,52 +99,7 @@ describe('NavigationStack', () => { store = configureStore( history, { nexus }, - { - dataExplorer: { - current: { - isDownloadable: false, - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.417881.3', - title: 'Allen Institute for Brain Science', - types: ['Agent', 'Organization'], - resource: [ - 'bbp', - 'atlas', - 'https://www.grid.ac/institutes/grid.417881.3', - 1, - ], - }, - links: [ - { - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', - title: '001_141216_A1_CA1py_R_MPG', - types: ['Entity', 'Trace', 'Dataset'], - resource: [ - 'bbp', - 'lnmce', - 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', - ], - }, - { - isDownloadable: false, - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', - title: 'Ecole Polytechnique Federale de Lausanne', - types: ['Organization', 'prov#Agent'], - resource: [ - 'bbp', - 'atlas', - 'https://www.grid.ac/institutes/grid.5333.6', - 1, - ], - }, - ], - shrinked: false, - highlightIndex: -1, - limited: false, - }, - } + { dataExplorer: initialDataExplorerState } ); app = ( @@ -110,7 +115,6 @@ describe('NavigationStack', () => { rerender = component.rerender; user = userEvent.setup(); }); - it('should render the correct number of NavigationStackItem components in the state', () => { const navigationItems = container.querySelectorAll( '.navigation-stack-item:not(.no-more)' @@ -244,4 +248,13 @@ describe('NavigationStack', () => { const state = store.getState(); expect(state.dataExplorer.current._self).toEqual(fourthItemInStack._self); }); + it('should decode the navigation digest at first render', () => { + store.dispatch(ResetDataExplorerGraphFlow()); + store.dispatch(PopulateDataExplorerGraphFlow(sampleDigest)); + render(app); + const dataExplorerState = store.getState().dataExplorer; + expect(dataExplorerState.links.length).toBe(2); + expect(dataExplorerState.current._self).toBe(digestCurrentSelf); + expect(dataExplorerState.shrinked).toBe(false); + }); }); From 3259f6e50a6c5e0de34c0b69761e15ee1dd00e19 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 12:57:55 +0200 Subject: [PATCH 028/192] de-10: reset data explorer state when quiting the page --- .../canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index f98ef5840..b1423afc8 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -6,6 +6,7 @@ import { RootState } from '../../store/reducers'; import { DATA_EXPLORER_GRAPH_FLOW_DIGEST, PopulateDataExplorerGraphFlow, + ResetDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; import { NavigationBack, @@ -36,6 +37,7 @@ const DataExplorerResolverPage = () => { useEffect(() => { const unlisten = history.listen(() => { + dispatch(ResetDataExplorerGraphFlow()); localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); }); return () => unlisten(); From 6f7a723cce756e018cf92aa19118b378bf16ff79 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 30 Jun 2023 16:17:17 +0200 Subject: [PATCH 029/192] de-9: fix typos --- .../DataExplorerGraphFlowMolecules/NavigationStackItem.tsx | 4 ++-- .../NavigationStack.spec.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx index a1c327c4e..e6ead3f1d 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -6,7 +6,7 @@ import { isArray } from 'lodash'; import { PlusOutlined } from '@ant-design/icons'; import { TDEResource, - JumpToNodeDataExplorerGrpahFlow, + JumpToNodeDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; import './styles.less'; @@ -32,7 +32,7 @@ const NavigationStackItem = ({ const { shrinked, links } = useSelector( (state: RootState) => state.dataExplorer ); - const onClick = () => dispatch(JumpToNodeDataExplorerGrpahFlow(index)); + const onClick = () => dispatch(JumpToNodeDataExplorerGraphFlow(index)); return (
{ } }); it('should the items in the stack be 4 when the user jump to the 5th item', () => { - store.dispatch(JumpToNodeDataExplorerGrpahFlow(4)); + store.dispatch(JumpToNodeDataExplorerGraphFlow(4)); rerender(app); const navigationStackShrinkedItem = container.querySelector( '.navigation-stack-item.more' From c71b536c149e358bc627ceb84428e6ba98598fef Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 30 Jun 2023 16:45:40 +0200 Subject: [PATCH 030/192] de-10: fix click outside popover --- src/shared/components/ResourceEditor/ResourceEditor.less | 2 +- src/shared/components/ResourceEditor/index.tsx | 1 - .../NavigationStackShrinkedItem.tsx | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 32b5ab3eb..1ea19013b 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -62,7 +62,7 @@ &.resolution-on-progress { .CodeMirror-lines { - cursor: progress; + cursor: progress !important; user-select: none; } } diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index e5f507f34..84f03de28 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -117,7 +117,6 @@ const ResourceEditor: React.FunctionComponent = props => { }; const onLinkClick = async (_: any, ev: MouseEvent) => { setLoadingResolution(true); - ev.stopPropagation(); const x = ev.pageX; const y = ev.pageY; const editorPosition = codeMirorRef.current?.coordsChar({ diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx index 746ea5c7a..97cb14df5 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx @@ -22,6 +22,7 @@ const NavigationStackShrinkedItem = () => { shrinked ? 'more' : 'no-more', shrinked && highlightIndex !== -1 && 'highlight' )} + hidden={!shrinked} > Date: Fri, 30 Jun 2023 16:50:38 +0200 Subject: [PATCH 031/192] de-10: clean code --- src/shared/components/ResourceEditor/editorUtils.ts | 1 - src/shared/modals/CreateStudio/CreateStudio.tsx | 1 - src/shared/organisms/DataPanel/DataPanel.tsx | 1 - src/shared/utils/datapanel.ts | 1 - src/subapps/admin/index.tsx | 1 - 5 files changed, 5 deletions(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index 9b6efe942..d4e6b5818 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -142,7 +142,6 @@ export async function resolveLinkInEditor({ }, }); } catch (error) { - console.error('case 3: ', url, externalLink(url), '\n', error); // case-3: if an error occured when tring both resolution method above // we check if the resource is external if (externalLink(url)) { diff --git a/src/shared/modals/CreateStudio/CreateStudio.tsx b/src/shared/modals/CreateStudio/CreateStudio.tsx index 2fb9ba409..71cf1b75f 100644 --- a/src/shared/modals/CreateStudio/CreateStudio.tsx +++ b/src/shared/modals/CreateStudio/CreateStudio.tsx @@ -136,7 +136,6 @@ const createStudioResource = async ({ generateStudioResource(label, description, plugins) ); } catch (error) { - console.log('@@error create studio', error); // @ts-ignore throw new Error('Can not process create studio request', { cause: error }); } diff --git a/src/shared/organisms/DataPanel/DataPanel.tsx b/src/shared/organisms/DataPanel/DataPanel.tsx index 1240f6a27..d7457f3af 100644 --- a/src/shared/organisms/DataPanel/DataPanel.tsx +++ b/src/shared/organisms/DataPanel/DataPanel.tsx @@ -481,7 +481,6 @@ const DataPanel: React.FC = ({}) => { }`, }; } catch (error) { - console.log('@@error', resource.id, error); return; } }); diff --git a/src/shared/utils/datapanel.ts b/src/shared/utils/datapanel.ts index 62daf5f37..8866689ac 100644 --- a/src/shared/utils/datapanel.ts +++ b/src/shared/utils/datapanel.ts @@ -133,7 +133,6 @@ export const toLocalStorageResources = ( }, ]; } catch (err) { - console.log('@error Failed to serialize resource for localStorage.', err); Sentry.captureException(err, { extra: { resource, diff --git a/src/subapps/admin/index.tsx b/src/subapps/admin/index.tsx index 523ea1918..565609f83 100644 --- a/src/subapps/admin/index.tsx +++ b/src/subapps/admin/index.tsx @@ -50,7 +50,6 @@ export const AdminSubappProviderHOC = (component: React.FunctionComponent) => { export const RedirectAdmin: React.FunctionComponent = props => { const location = useLocation(); const route = useRouteMatch(); - console.log('@@location', get(route.params, '0'), route); return ( Date: Mon, 3 Jul 2023 15:19:16 +0200 Subject: [PATCH 032/192] fix: test must be independent from eachother --- .../DateExplorerGraphFlow.tsx | 2 +- .../NavigationStackItem.tsx | 1 + .../NavigationStackShrinkedItem.tsx | 8 +- .../NavigationStack.spec.tsx | 287 ++++++++++++++---- src/shared/store/reducers/data-explorer.ts | 4 +- 5 files changed, 242 insertions(+), 60 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index b1423afc8..ae169ad12 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -37,7 +37,7 @@ const DataExplorerResolverPage = () => { useEffect(() => { const unlisten = history.listen(() => { - dispatch(ResetDataExplorerGraphFlow()); + dispatch(ResetDataExplorerGraphFlow({ initialState: null })); localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); }); return () => unlisten(); diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx index e6ead3f1d..c11fa5211 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -38,6 +38,7 @@ const NavigationStackItem = ({
{ Open more {count} other resources
} + title={
Open {count} other resources
} > - +
{count}
diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index b53c1b399..9fa90c64c 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -1,25 +1,28 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { render, RenderResult, within } from '@testing-library/react'; - +import { render, RenderResult } from '@testing-library/react'; +import { AnyAction, Store } from 'redux'; import { Provider } from 'react-redux'; -import NavigationStack from './NavigationStack'; -import { createMemoryHistory } from 'history'; -import { createNexusClient } from '@bbp/nexus-sdk'; -import { deltaPath } from '__mocks__/handlers/handlers'; -import configureStore from '../../../shared/store'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import { createNexusClient, NexusClient } from '@bbp/nexus-sdk'; import { Router } from 'react-router-dom'; +import userEvent from '@testing-library/user-event'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import { NexusProvider } from '@bbp/react-nexus'; +import { waitFor } from '../../../utils/testUtil'; +import { deltaPath } from '../../../__mocks__/handlers/handlers'; +import NavigationStack from './NavigationStack'; +import { + NavigationBack, + NavigationCollapseButton, +} from '../../molecules/DataExplorerGraphFlowMolecules'; +import configureStore from '../../store'; import { - JumpToNodeDataExplorerGraphFlow, ReturnBackDataExplorerGraphFlow, AddNewNodeDataExplorerGraphFlow, ResetDataExplorerGraphFlow, PopulateDataExplorerGraphFlow, -} from '../../../shared/store/reducers/data-explorer'; -import { AnyAction, Store } from 'redux'; -import userEvent from '@testing-library/user-event'; -import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +} from '../../store/reducers/data-explorer'; const sampleDigest = 'eyJjdXJyZW50Ijp7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6dm9sdW1ldHJpY2RhdGFsYXllci9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLCJ0aXRsZSI6IkJCUCBNb3VzZSBCcmFpbiBUZW1wbGF0ZSBWb2x1bWUsIDI1bSIsInR5cGVzIjpbIlZvbHVtZXRyaWNEYXRhTGF5ZXIiLCJCcmFpblRlbXBsYXRlRGF0YUxheWVyIiwiRGF0YXNldCJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLDJdfSwibGlua3MiOlt7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2xubWNlL2RhdGFzaGFwZXM6ZGF0YXNldC90cmFjZXMlMkY0NjBiZmEyZS1jYjdkLTQ0MjAtYTQ0OC0yMDMwYTZiZjRhZTQiLCJ0aXRsZSI6IjAwMV8xNDEyMTZfQTFfQ0ExcHlfUl9NUEciLCJ0eXBlcyI6WyJFbnRpdHkiLCJUcmFjZSIsIkRhdGFzZXQiXSwicmVzb3VyY2UiOlsiYmJwIiwibG5tY2UiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvdHJhY2VzLzQ2MGJmYTJlLWNiN2QtNDQyMC1hNDQ4LTIwMzBhNmJmNGFlNCIsMTVdfSx7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6YXRsYXNzcGF0aWFscmVmZXJlbmNlc3lzdGVtL2FsbGVuX2NjZnYzX3NwYXRpYWxfcmVmZXJlbmNlX3N5c3RlbSIsInRpdGxlIjoiQWxsZW4gTW91c2UgQ0NGIiwidHlwZXMiOlsiQXRsYXNTcGF0aWFsUmVmZXJlbmNlU3lzdGVtIiwiQnJhaW5BdGxhc1NwYXRpYWxSZWZlcmVuY2VTeXN0ZW0iXSwicmVzb3VyY2UiOlsiYmJwIiwiYXRsYXMiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvYWxsZW5fY2NmdjNfc3BhdGlhbF9yZWZlcmVuY2Vfc3lzdGVtIiw5XX1dLCJzaHJpbmtlZCI6ZmFsc2UsImhpZ2hsaWdodEluZGV4IjotMX0='; @@ -89,10 +92,13 @@ describe('NavigationStack', () => { let rerender: (ui: React.ReactElement) => void; let store: Store; let user: UserEvent; + let history: MemoryHistory<{}>; + let nexus: NexusClient; + beforeAll(() => { - const history = createMemoryHistory({}); + history = createMemoryHistory({}); - const nexus = createNexusClient({ + nexus = createNexusClient({ fetch, uri: deltaPath(), }); @@ -105,7 +111,11 @@ describe('NavigationStack', () => { - + <> + + + + @@ -115,13 +125,25 @@ describe('NavigationStack', () => { rerender = component.rerender; user = userEvent.setup(); }); + beforeEach(() => { + // reset the data explorer state + store.dispatch(ResetDataExplorerGraphFlow({ initialState: null })); + }); it('should render the correct number of NavigationStackItem components in the state', () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); + rerender(app); const navigationItems = container.querySelectorAll( '.navigation-stack-item:not(.no-more)' ); expect(navigationItems.length).toBe(2); }); it('should render the correct number of NavigationStackItem components after hit the return back btn', () => { + // create a new store with preloaded state + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); store.dispatch(ReturnBackDataExplorerGraphFlow()); rerender(app); const navigationItemsAfterBack = container.querySelectorAll( @@ -130,6 +152,9 @@ describe('NavigationStack', () => { expect(navigationItemsAfterBack.length).toBe(1); }); it('should render the correct number of NavigationStackItem after multiple navigation', () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -140,7 +165,6 @@ describe('NavigationStack', () => { resource: ['bbp', 'atlas', 'https://bbp.neuroshapes.org', 1], }) ); - rerender(app); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -156,17 +180,29 @@ describe('NavigationStack', () => { ], }) ); - render(app); store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); - render(app); + rerender(app); const navigationItemsAfterMultipleNavigation = container.querySelectorAll( '.navigation-stack-item:not(.no-more)' ); - expect(navigationItemsAfterMultipleNavigation.length).toBe(4); + expect(navigationItemsAfterMultipleNavigation.length).toBe(5); const state = store.getState(); - expect(state.dataExplorer.links.length).toBe(4); + expect(state.dataExplorer.links.length).toBe(5); }); it('should render the NavigationStackShrinkedItem when it passed MAX_NAVIGATION_ITEMS_IN_STACK', () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/_/https:%2F%2Fbbp.neuroshapes.org', + title: 'bbp.neuroshapes.org', + types: [], + resource: ['bbp', 'atlas', 'https://bbp.neuroshapes.org', 1], + }) + ); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -182,7 +218,22 @@ describe('NavigationStack', () => { ], }) ); - rerender(app); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/_/https:%2F%2Fneuroshapes.org', + title: 'neuroshapes.org', + types: [], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://neuroshapes.org', + 161, + ], + }) + ); + store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -207,49 +258,175 @@ describe('NavigationStack', () => { const navigationHiddenItems = container.querySelectorAll( '.navigation-stack-item:not(.no-more):not(.more)[hidden]' ); - expect(navigationHiddenItems.length).toBe(4); + expect(navigationHiddenItems.length).toBe(5); }); - it('should show the collapse button when the NavigationStackShrinkedItem is clicked', () => { - const navigationStackShrinkedItem = container.querySelector( - '.navigation-stack-item.more' + it('should show the collapse button when the NavigationStackShrinkedItem is clicked', async () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) ); - if (navigationStackShrinkedItem) { - user.click(navigationStackShrinkedItem); - const collapseBtn = container.querySelector('.navigation-collapse-btn'); - expect(collapseBtn).not.toBeNull(); - expect(collapseBtn).toBeInTheDocument(); - expect(store.getState().dataExplorer.shrinked).toBe(false); - } - }); - it('should hide the collapse button when the collapse button is clicked', () => { - const navigationCollapseButton = container.querySelector( - '.navigation-collapse-btn' + rerender(app); + // console.log('@@dada', JSON.stringify(store.getState().dataExplorer, null, 2)); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/_/https:%2F%2Fbbp.neuroshapes.org', + title: 'bbp.neuroshapes.org', + types: [], + resource: ['bbp', 'atlas', 'https://bbp.neuroshapes.org', 1], + }) + ); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/datashapes:ontologyentity/https:%2F%2Fbbp.epfl.ch%2Fontologies%2Fcore%2Fefeatures%2FNeuroElectroNeuronElectrophysiologicalFeature', + title: 'NeuroElectro Neuron Electrophysiological Feature', + types: ['Class'], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://bbp.epfl.ch/ontologies/core/efeatures/NeuroElectroNeuronElectrophysiologicalFeature', + 29, + ], + }) + ); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/_/https:%2F%2Fneuroshapes.org', + title: 'neuroshapes.org', + types: [], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://neuroshapes.org', + 161, + ], + }) + ); + store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:atlasrelease/4906ab85-694f-469d-962f-c0174e901885', + title: 'Blue Brain Atlas', + types: ['AtlasRelease', 'BrainAtlasRelease'], + resource: [ + 'bbp', + 'atlas', + 'https://bbp.epfl.ch/neurosciencegraph/data/4906ab85-694f-469d-962f-c0174e901885', + 3, + ], + }) ); - if (navigationCollapseButton) { - user.click(navigationCollapseButton); - const collapseBtn = container.querySelector('.navigation-collapse-btn'); - expect(collapseBtn).toBeNull(); - expect(store.getState().dataExplorer.shrinked).toBe(true); - } - }); - it('should the items in the stack be 4 when the user jump to the 5th item', () => { - store.dispatch(JumpToNodeDataExplorerGraphFlow(4)); rerender(app); - const navigationStackShrinkedItem = container.querySelector( - '.navigation-stack-item.more' + const openShrinkedNavList = container.querySelector( + '[role="open-shrinked-nav"]' ); - expect(navigationStackShrinkedItem).toBeNull(); - const navigationItems = container.querySelectorAll( - '.navigation-stack-item:not(.no-more)' + console.log( + '@@navigationStackShrinkedItem', + openShrinkedNavList?.innerHTML ); - expect(navigationItems.length).toBe(4); + await waitFor(async () => { + if (openShrinkedNavList) { + await user.click(openShrinkedNavList); + rerender(app); + const collapseBtn = container.querySelector('.navigation-collapse-btn'); + console.log('@@collapseBtn', collapseBtn?.innerHTML); + console.log( + '@@state', + JSON.stringify(store.getState().dataExplorer, null, 2) + ); + expect(collapseBtn).not.toBeNull(); + expect(collapseBtn).toBeInTheDocument(); + expect(store.getState().dataExplorer.shrinked).toBe(false); + } + }); }); - it('should the fourth item in the stack will be the current one when the user jump to the 4th item', () => { - const state = store.getState(); - expect(state.dataExplorer.current._self).toEqual(fourthItemInStack._self); + it('should the items in the stack be 4 when the user jump to the 5th item', async () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/_/https:%2F%2Fbbp.neuroshapes.org', + title: 'bbp.neuroshapes.org', + types: [], + resource: ['bbp', 'atlas', 'https://bbp.neuroshapes.org', 1], + }) + ); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/datashapes:ontologyentity/https:%2F%2Fbbp.epfl.ch%2Fontologies%2Fcore%2Fefeatures%2FNeuroElectroNeuronElectrophysiologicalFeature', + title: 'NeuroElectro Neuron Electrophysiological Feature', + types: ['Class'], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://bbp.epfl.ch/ontologies/core/efeatures/NeuroElectroNeuronElectrophysiologicalFeature', + 29, + ], + }) + ); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/_/https:%2F%2Fneuroshapes.org', + title: 'neuroshapes.org', + types: [], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://neuroshapes.org', + 161, + ], + }) + ); + store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); + store.dispatch( + AddNewNodeDataExplorerGraphFlow({ + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:atlasrelease/4906ab85-694f-469d-962f-c0174e901885', + title: 'Blue Brain Atlas', + types: ['AtlasRelease', 'BrainAtlasRelease'], + resource: [ + 'bbp', + 'atlas', + 'https://bbp.epfl.ch/neurosciencegraph/data/4906ab85-694f-469d-962f-c0174e901885', + 3, + ], + }) + ); + rerender(app); + await waitFor(async () => { + const forthNode = container.querySelector( + '.navigation-stack-item.item-4 > .anticon.anticon-plus' + ); + if (forthNode) { + await user.click(forthNode); + rerender(app); + const navigationItems = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItems.length).toBe(4); + const state = store.getState(); + expect(state.dataExplorer.current._self).toEqual( + fourthItemInStack._self + ); + } + }); }); it('should decode the navigation digest at first render', () => { - store.dispatch(ResetDataExplorerGraphFlow()); + store.dispatch(ResetDataExplorerGraphFlow({ initialState: null })); store.dispatch(PopulateDataExplorerGraphFlow(sampleDigest)); render(app); const dataExplorerState = store.getState().dataExplorer; diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index bfaf9a510..a4cc89f5e 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -143,8 +143,8 @@ export const dataExplorerSlice = createSlice({ calculateNewDigest(newState); return newState; }, - ResetDataExplorerGraphFlow: () => { - return initialState; + ResetDataExplorerGraphFlow: (state, action) => { + return action.payload.initialState ?? initialState; }, ResetHighlightedNodeDataExplorerGraphFlow: state => { return { From 127a2e0f46fd31ff0140c9716801c64b5e053d1f Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 3 Jul 2023 15:40:50 +0200 Subject: [PATCH 033/192] fix: test for navigation tasks --- .../NavigationStack.spec.tsx | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index 9fa90c64c..aa3ba0bab 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -95,7 +95,7 @@ describe('NavigationStack', () => { let history: MemoryHistory<{}>; let nexus: NexusClient; - beforeAll(() => { + beforeEach(() => { history = createMemoryHistory({}); nexus = createNexusClient({ @@ -105,7 +105,7 @@ describe('NavigationStack', () => { store = configureStore( history, { nexus }, - { dataExplorer: initialDataExplorerState } + { } ); app = ( @@ -125,10 +125,7 @@ describe('NavigationStack', () => { rerender = component.rerender; user = userEvent.setup(); }); - beforeEach(() => { - // reset the data explorer state - store.dispatch(ResetDataExplorerGraphFlow({ initialState: null })); - }); + it('should render the correct number of NavigationStackItem components in the state', () => { store.dispatch( ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) @@ -265,7 +262,6 @@ describe('NavigationStack', () => { ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) ); rerender(app); - // console.log('@@dada', JSON.stringify(store.getState().dataExplorer, null, 2)); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -326,25 +322,12 @@ describe('NavigationStack', () => { const openShrinkedNavList = container.querySelector( '[role="open-shrinked-nav"]' ); - console.log( - '@@navigationStackShrinkedItem', - openShrinkedNavList?.innerHTML - ); - await waitFor(async () => { - if (openShrinkedNavList) { - await user.click(openShrinkedNavList); - rerender(app); - const collapseBtn = container.querySelector('.navigation-collapse-btn'); - console.log('@@collapseBtn', collapseBtn?.innerHTML); - console.log( - '@@state', - JSON.stringify(store.getState().dataExplorer, null, 2) - ); - expect(collapseBtn).not.toBeNull(); - expect(collapseBtn).toBeInTheDocument(); - expect(store.getState().dataExplorer.shrinked).toBe(false); - } - }); + openShrinkedNavList && await user.click(openShrinkedNavList); + rerender(app); + const collapseBtn = container.querySelector('.navigation-collapse-btn'); + expect(collapseBtn).not.toBeNull(); + expect(collapseBtn).toBeInTheDocument(); + expect(store.getState().dataExplorer.shrinked).toBe(false); }); it('should the items in the stack be 4 when the user jump to the 5th item', async () => { store.dispatch( From c54e1c0365d1220101a74ed1d4541270a059ad19 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 3 Jul 2023 15:53:40 +0200 Subject: [PATCH 034/192] refactor: fix formatting --- .../NavigationStack.spec.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index aa3ba0bab..0d6388e03 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -102,11 +102,7 @@ describe('NavigationStack', () => { fetch, uri: deltaPath(), }); - store = configureStore( - history, - { nexus }, - { } - ); + store = configureStore(history, { nexus }, {}); app = ( @@ -322,7 +318,7 @@ describe('NavigationStack', () => { const openShrinkedNavList = container.querySelector( '[role="open-shrinked-nav"]' ); - openShrinkedNavList && await user.click(openShrinkedNavList); + openShrinkedNavList && (await user.click(openShrinkedNavList)); rerender(app); const collapseBtn = container.querySelector('.navigation-collapse-btn'); expect(collapseBtn).not.toBeNull(); From b2663697e1d74aa9f70dbdbd0a5c0d0f54f89971 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 3 Jul 2023 17:41:10 +0200 Subject: [PATCH 035/192] fix: dispatch action by clicking the back btn instead of using the store --- .../NavigationStack.spec.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index 0d6388e03..17c43d77f 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -132,12 +132,12 @@ describe('NavigationStack', () => { ); expect(navigationItems.length).toBe(2); }); - it('should render the correct number of NavigationStackItem components after hit the return back btn', () => { - // create a new store with preloaded state + it('should render the correct number of NavigationStackItem components after hit the return back btn', async () => { store.dispatch( ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) ); - store.dispatch(ReturnBackDataExplorerGraphFlow()); + const navigationBackItem = container.querySelector('.navigation-back-btn'); + navigationBackItem && (await user.click(navigationBackItem)); rerender(app); const navigationItemsAfterBack = container.querySelectorAll( '.navigation-stack-item:not(.no-more)' From d552af970cda5971e53377d91ff315334048f3f5 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 4 Jul 2023 09:34:09 +0200 Subject: [PATCH 036/192] fix: test for element selected for action --- .../NavigationStack.spec.tsx | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index 17c43d77f..aa5341562 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -18,7 +18,6 @@ import { } from '../../molecules/DataExplorerGraphFlowMolecules'; import configureStore from '../../store'; import { - ReturnBackDataExplorerGraphFlow, AddNewNodeDataExplorerGraphFlow, ResetDataExplorerGraphFlow, PopulateDataExplorerGraphFlow, @@ -318,6 +317,8 @@ describe('NavigationStack', () => { const openShrinkedNavList = container.querySelector( '[role="open-shrinked-nav"]' ); + expect(openShrinkedNavList).not.toBeNull(); + expect(openShrinkedNavList).toBeInTheDocument(); openShrinkedNavList && (await user.click(openShrinkedNavList)); rerender(app); const collapseBtn = container.querySelector('.navigation-collapse-btn'); @@ -339,6 +340,7 @@ describe('NavigationStack', () => { resource: ['bbp', 'atlas', 'https://bbp.neuroshapes.org', 1], }) ); + store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -369,7 +371,6 @@ describe('NavigationStack', () => { ], }) ); - store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -386,23 +387,19 @@ describe('NavigationStack', () => { }) ); rerender(app); - await waitFor(async () => { - const forthNode = container.querySelector( - '.navigation-stack-item.item-4 > .anticon.anticon-plus' - ); - if (forthNode) { - await user.click(forthNode); - rerender(app); - const navigationItems = container.querySelectorAll( - '.navigation-stack-item:not(.no-more)' - ); - expect(navigationItems.length).toBe(4); - const state = store.getState(); - expect(state.dataExplorer.current._self).toEqual( - fourthItemInStack._self - ); - } - }); + const forthNode = container.querySelector( + '.navigation-stack-item.item-4 > .anticon.anticon-plus' + ); + expect(forthNode).not.toBeNull(); + expect(forthNode).toBeInTheDocument(); + forthNode && (await user.click(forthNode)); + rerender(app); + const navigationItems = container.querySelectorAll( + '.navigation-stack-item:not(.no-more)' + ); + expect(navigationItems.length).toBe(4); + const state = store.getState(); + expect(state.dataExplorer.current._self).toEqual(fourthItemInStack._self); }); it('should decode the navigation digest at first render', () => { store.dispatch(ResetDataExplorerGraphFlow({ initialState: null })); From c4c0fc6c8cd879480a052808f98e6e9f04443e04 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 4 Jul 2023 12:24:47 +0200 Subject: [PATCH 037/192] fix: enable back link detection and resolution --- src/shared/components/ResourceEditor/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 1570b483f..84f03de28 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -263,8 +263,8 @@ const ResourceEditor: React.FunctionComponent = props => { handleChange={handleChange} keyFoldCode={keyFoldCode} loadingResolution={loadingResolution} - onLinkClick={() => {}} - onLinksFound={() => {}} + onLinkClick={onLinkClick} + onLinksFound={onLinksFound} ref={codeMirorRef} />
From cb2e255777a71046acde7facb88b0fa3dca2b20b Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 11:46:22 +0200 Subject: [PATCH 038/192] de-dn: add file extension to file downloaded name --- src/shared/utils/download.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/shared/utils/download.ts b/src/shared/utils/download.ts index 4c5c35fc3..2de99721f 100644 --- a/src/shared/utils/download.ts +++ b/src/shared/utils/download.ts @@ -1,11 +1,14 @@ +import { fileExtensionFromResourceEncoding } from '../../utils/contentTypes'; + export const download = (filename: string, mediaType: string, data: any) => { const blob = new Blob([data], { type: mediaType }); + const extention = fileExtensionFromResourceEncoding(mediaType); if (window.navigator.msSaveBlob) { window.navigator.msSaveBlob(blob, filename); } else { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); - link.download = filename; + link.download = extention ? `${filename}.${extention}` : filename; link.click(); } }; From 82ce80fafe46c5fb241f5fd23e2466075450c20e Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 11:47:00 +0200 Subject: [PATCH 039/192] de-dn: add option to detect files in url resolution --- src/shared/components/ResourceEditor/index.tsx | 7 +++++++ src/shared/store/reducers/data-explorer.ts | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 1570b483f..ff368e767 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -17,6 +17,7 @@ import 'codemirror/addon/fold/brace-fold'; import isValidUrl from '../../../utils/validUrl'; import CodeEditor from './CodeEditor'; import { TToken, resolveLinkInEditor } from './editorUtils'; +import { Resource } from '@bbp/nexus-sdk'; import './ResourceEditor.less'; export interface ResourceEditorProps { @@ -42,6 +43,12 @@ export const LINE_HEIGHT = 50; export const INDENT_UNIT = 4; const switchMarginRight = { marginRight: 5 }; +const isDownloadableLink = (resource: Resource) => { + return Boolean( + resource['@type'] === 'File' || resource['@type']?.includes('File') + ); +}; + const ResourceEditor: React.FunctionComponent = props => { const { rawData, diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index a4cc89f5e..152d5b1a3 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -5,12 +5,18 @@ type TProject = string; type TOrganization = string; type TResourceID = string; type TVersionTag = number; -export type TDEResource = [TOrganization, TProject, TResourceID, TVersionTag]; +type TMediaType = string; + +export type TDEResource = + | [TOrganization, TProject, TResourceID, TVersionTag] + | [TOrganization, TProject, TResourceID, TVersionTag, TMediaType]; + export type TDELink = { _self: string; title: string; types?: string | string[]; resource?: TDEResource; + isDownloadable: boolean; }; export type TDataExplorerState = { links: TDELink[]; From 5e835d2cce282f54635cee29f368b353843d8363 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 11:48:06 +0200 Subject: [PATCH 040/192] de-dn: allow File download in url resolution popover --- .../ResolvedLinkEditorPopover.tsx | 58 ++++++++++++++++++- .../ResolvedLinkEditorPopover/styles.less | 46 +++++++++++++-- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx index 704b0776a..71c16d929 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx @@ -2,9 +2,10 @@ import React, { ReactNode, useRef, forwardRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory, useLocation, useRouteMatch } from 'react-router'; import { useNexusContext } from '@bbp/react-nexus'; -import { Resource } from '@bbp/nexus-sdk'; +import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { clsx } from 'clsx'; -import { Tag } from 'antd'; +import { Tag, Divider } from 'antd'; +import { DownloadOutlined } from '@ant-design/icons'; import { match as pmatch } from 'ts-pattern'; import { UISettingsActionTypes } from '../../store/actions/ui-settings'; import { RootState } from '../../store/reducers'; @@ -19,6 +20,8 @@ import { } from '../../store/reducers/ui-settings'; import { getOrgAndProjectFromProjectId, getResourceLabel } from '../../utils'; import { getNormalizedTypes } from '../../components/ResourceEditor/editorUtils'; +import { download } from '../../utils/download'; +import { parseResourceId } from '../../components/Preview/Preview'; import useOnClickOutside from '../../hooks/useClickOutside'; import './styles.less'; @@ -28,6 +31,34 @@ type PopoverContainer = { onClickOutside(): void; }; +const downloadFile = async ({ + nexus, + orgLabel, + projectLabel, + resourceId, + ext, + title, +}: { + nexus: NexusClient; + orgLabel: string; + projectLabel: string; + resourceId: string; + title: string; + ext?: string; +}) => { + try { + const data = await nexus.File.get( + orgLabel, + projectLabel, + encodeURIComponent(parseResourceId(resourceId)), + { as: 'blob' } + ); + return download(title, ext ?? 'json', data); + } catch (error) { + console.log('@@error', error); + } +}; + const PopoverContainer = forwardRef( ({ children, onClickOutside }, ref) => { const { @@ -105,10 +136,21 @@ const ResolvedLinkEditorPopover = () => { } }; + const onDownload = async (data: TDELink) => { + await downloadFile({ + nexus, + orgLabel: data.resource?.[0]!, + projectLabel: data.resource?.[1]!, + resourceId: data.resource?.[2]!, + ext: data.resource?.[4] ?? 'json', + title: data.title, + }); + }; + return pmatch(resultPattern) .with({ open: true, resolvedAs: 'error' }, () => ( -
{error}
+
{error}
)) .with({ open: true, resolvedAs: 'resource' }, () => { @@ -125,6 +167,11 @@ const ResolvedLinkEditorPopover = () => { > {result.title ?? result.resource?.[2]} + {result.isDownloadable && ( +
+ onDownload(result)} /> +
+ )}
); @@ -143,6 +190,11 @@ const ResolvedLinkEditorPopover = () => { > {item.title ?? item.resource?.[2]} + {item.isDownloadable && ( +
+ onDownload(item)} /> +
+ )}
))}
diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less index 0880f0621..634730973 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less +++ b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less @@ -1,9 +1,10 @@ +@import '../../lib.less'; + .custom-popover-token { position: absolute; background: white; box-shadow: 0px 2px 12px rgba(51, 51, 51, 0.12); border-radius: 6px; - padding: 10px 12px; z-index: 9999; max-width: 580px; display: flex; @@ -16,11 +17,16 @@ align-items: center; justify-content: flex-start; gap: 2px; - margin-bottom: 3px; - padding: 0 4px; + // margin-bottom: 3px; + // padding: 0 4px; width: 100%; overflow: hidden; - .link { + + span.ant-tag { + margin: 10px 12px; + } + + .popover-btn.link { border: none; margin: 0; padding: 0; @@ -28,16 +34,22 @@ overflow: visible; background: transparent; cursor: pointer; - color: blue; + color: @fusion-daybreak-7; white-space: pre-line; text-overflow: ellipsis; line-clamp: 1; white-space: nowrap; overflow: hidden; width: fit-content; + padding: 10px 10px 10px 0; } } + .popover-btn.error { + padding: 10px; + user-select: none; + } + .external { flex-direction: column; align-items: flex-start; @@ -50,4 +62,28 @@ background-color: rgba(255, 0, 0, 0.478); color: white; } + + .popover-download-btn { + position: relative; + /* top: -0.06em; */ + display: inline-block; + height: auto; + align-self: stretch; + margin: 0 0 0 5px; + vertical-align: middle; + border-top: 0; + border-left: 1px solid rgba(0, 0, 0, 0.06); + display: flex; + align-items: center; + justify-content: center; + background-color: @fusion-main-color; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + padding: 4px 12px; + + svg { + font-size: 18px; + color: white; + } + } } From c17a484a77ee28410668d560b845b32d9cb5460e Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sat, 24 Jun 2023 11:48:28 +0200 Subject: [PATCH 041/192] de-dn: allow name extraction from _filename --- src/shared/utils/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index 89f9b9452..c79a252d2 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -301,6 +301,7 @@ export function getResourceLabel( resource.prefLabel ?? resource.label ?? resourceName ?? + resource._filename ?? labelOf(resource['@id']) ?? labelOf(resource._self) ); From e069731a79d74a0ed90022ab2a39f3b420a8596e Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 26 Jun 2023 11:43:35 +0200 Subject: [PATCH 042/192] de-dn: fix styles for list of resources --- .../ResolvedLinkEditorPopover.tsx | 2 +- .../ResolvedLinkEditorPopover/styles.less | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx index 71c16d929..a5e5cc8c8 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx @@ -180,7 +180,7 @@ const ResolvedLinkEditorPopover = () => { return ( {(results as TDELink[]).map(item => ( -
+
{item.resource?.[0] && item.resource?.[1] && ( {`${item.resource?.[0]}/${item.resource?.[1]}`} )} diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less index 634730973..447ccdb77 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less +++ b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less @@ -21,11 +21,28 @@ // padding: 0 4px; width: 100%; overflow: hidden; - span.ant-tag { margin: 10px 12px; } + &.list-item { + &:first-child { + span.ant-tag { + margin: 10px 12px 5px; + } + } + &:last-child { + span.ant-tag { + margin: 5px 12px 10px; + } + } + span.ant-tag { + margin: 3px 12px; + } + .popover-btn.link { + padding: 3px 10px 3px 0; + } + } .popover-btn.link { border: none; margin: 0; From 0e30dacebf351235d762e2da7878ff56a6bdf06f Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 26 Jun 2023 16:22:33 +0200 Subject: [PATCH 043/192] de-dn: rename the DateExplorerResolver to DateExplorerGraphFlow --- .../DataExplorerGraphFlowPage/DataExplorerGraphFlowPage.tsx | 4 ++-- .../canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/DataExplorerGraphFlowPage/DataExplorerGraphFlowPage.tsx b/src/pages/DataExplorerGraphFlowPage/DataExplorerGraphFlowPage.tsx index 965a80dfd..af892fe54 100644 --- a/src/pages/DataExplorerGraphFlowPage/DataExplorerGraphFlowPage.tsx +++ b/src/pages/DataExplorerGraphFlowPage/DataExplorerGraphFlowPage.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import DataExplorerResolverPage from '../../shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow'; +import DataExplorerGraphFlow from '../../shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow'; const DataExplorerGraphFlowPage = () => { - return ; + return ; }; export default DataExplorerGraphFlowPage; diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index ae169ad12..fbdc89cd6 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -16,7 +16,7 @@ import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStac import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; import './styles.less'; -const DataExplorerResolverPage = () => { +const DataExplorerGraphFlow = () => { const history = useHistory(); const location = useLocation(); const dispatch = useDispatch(); @@ -70,4 +70,4 @@ const DataExplorerResolverPage = () => { ); }; -export default DataExplorerResolverPage; +export default DataExplorerGraphFlow; From 70e58a837ffc46961704ed06d412cca6085e980d Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 26 Jun 2023 16:24:29 +0200 Subject: [PATCH 044/192] de-dn: all full screen to use the limited data-explorer field --- .../components/ResourceEditor/index.tsx | 7 +-- src/shared/containers/ResourceEditor.tsx | 46 +++++++++++-------- .../ContentLimitedHeader.tsx | 15 ++++-- .../styles.less | 1 + src/shared/store/reducers/data-explorer.ts | 9 ++++ 5 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index ff368e767..c69c12b8a 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -6,7 +6,7 @@ import { SaveOutlined, } from '@ant-design/icons'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useNexusContext } from '@bbp/react-nexus'; import codemiror from 'codemirror'; @@ -18,6 +18,7 @@ import isValidUrl from '../../../utils/validUrl'; import CodeEditor from './CodeEditor'; import { TToken, resolveLinkInEditor } from './editorUtils'; import { Resource } from '@bbp/nexus-sdk'; +import { RootState } from '../../store/reducers'; import './ResourceEditor.less'; export interface ResourceEditorProps { @@ -72,12 +73,12 @@ const ResourceEditor: React.FunctionComponent = props => { const nexus = useNexusContext(); const [loadingResolution, setLoadingResolution] = React.useState(false); const [isEditing, setEditing] = React.useState(editing); - const [fullScreen, setFullScreen] = React.useState(false); const [valid, setValid] = React.useState(true); const [parsedValue, setParsedValue] = React.useState(rawData); const [stringValue, setStringValue] = React.useState( JSON.stringify(rawData, null, 2) ); + const { limited } = useSelector((state: RootState) => state.dataExplorer); const dispatch = useDispatch(); const keyFoldCode = (cm: any) => { cm.foldCode(cm.getCursor()); @@ -215,7 +216,7 @@ const ResourceEditor: React.FunctionComponent = props => { diff --git a/src/shared/containers/ResourceEditor.tsx b/src/shared/containers/ResourceEditor.tsx index 879f3bc3e..615ae87d8 100644 --- a/src/shared/containers/ResourceEditor.tsx +++ b/src/shared/containers/ResourceEditor.tsx @@ -5,13 +5,16 @@ import { Resource, NexusClient, } from '@bbp/nexus-sdk'; -import { useHistory } from 'react-router'; +import { useHistory, useLocation } from 'react-router'; import { useDispatch } from 'react-redux'; import ResourceEditor from '../components/ResourceEditor'; import { useNexusContext } from '@bbp/react-nexus'; import { getNormalizedTypes } from '../components/ResourceEditor/editorUtils'; import useNotification, { parseNexusError } from '../hooks/useNotification'; -import { InitNewVisitDataExplorerGraphView } from '../store/reducers/data-explorer'; +import { + InitDataExplorerGraphFlowLimitedVersion, + InitNewVisitDataExplorerGraphView, +} from '../store/reducers/data-explorer'; import { getOrgAndProjectFromResourceObject, getResourceLabel } from '../utils'; const ResourceEditorContainer: React.FunctionComponent<{ @@ -46,6 +49,7 @@ const ResourceEditorContainer: React.FunctionComponent<{ const nexus = useNexusContext(); const dispatch = useDispatch(); const navigate = useHistory(); + const location = useLocation(); const notification = useNotification(); const [expanded, setExpanded] = React.useState(defaultExpanded); const [editable, setEditable] = React.useState(defaultEditable); @@ -119,23 +123,27 @@ const ResourceEditorContainer: React.FunctionComponent<{ } )) as Resource; const orgProject = getOrgAndProjectFromResourceObject(data); - dispatch( - InitNewVisitDataExplorerGraphView({ - current: { - _self: data._self, - types: getNormalizedTypes(data['@type']), - title: getResourceLabel(data), - resource: [ - orgProject?.orgLabel ?? '', - orgProject?.projectLabel ?? '', - data['@id'], - data._rev, - ], - }, - limited: true, - }) - ); - navigate.push('/data-explorer/graph-flow'); + if (location.pathname === '/data-explorer/graph-flow') { + dispatch(InitDataExplorerGraphFlowLimitedVersion(true)); + } else { + dispatch( + InitNewVisitDataExplorerGraphView({ + current: { + _self: data._self, + types: getNormalizedTypes(data['@type']), + title: getResourceLabel(data), + resource: [ + orgProject?.orgLabel ?? '', + orgProject?.projectLabel ?? '', + data['@id'], + data._rev, + ], + }, + limited: true, + }) + ); + navigate.push('/data-explorer/graph-flow'); + } }; async function getResourceSource( nexus: NexusClient, diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentLimitedHeader.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentLimitedHeader.tsx index 492e11a7a..41914dedf 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentLimitedHeader.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentLimitedHeader.tsx @@ -1,20 +1,25 @@ import React, { useState } from 'react'; import { Switch } from 'antd'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { RootState } from 'shared/store/reducers'; +import { InitDataExplorerGraphFlowLimitedVersion } from '../../store/reducers/data-explorer'; import './styles.less'; const DataExplorerGraphFlowContentLimitedHeader = () => { - const { current } = useSelector((state: RootState) => state.dataExplorer); - const [write, setWrite] = useState(false); - const onSelectWrite = (checked: boolean) => setWrite(() => checked); + const dispatch = useDispatch(); + const { current, limited } = useSelector( + (state: RootState) => state.dataExplorer + ); + const onSelectWrite = (checked: boolean) => { + dispatch(InitDataExplorerGraphFlowLimitedVersion(!checked)); + }; return (
{current?.title}
Read - + Write
diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less index e0e341915..98ac85b59 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less @@ -5,6 +5,7 @@ gap: 10px; left: 0; top: 52px; + z-index: 6; } .navigation-stack { diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 152d5b1a3..c68a9e29a 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -158,6 +158,14 @@ export const dataExplorerSlice = createSlice({ highlightIndex: -1, }; }, + InitDataExplorerGraphFlowLimitedVersion: (state, action) => { + const newState = { + ...state, + limited: action.payload ?? !state.limited, + }; + calculateNewDigest(newState); + return newState; + }, }, }); export const { @@ -170,6 +178,7 @@ export const { ReturnBackDataExplorerGraphFlow, ResetDataExplorerGraphFlow, ResetHighlightedNodeDataExplorerGraphFlow, + InitDataExplorerGraphFlowLimitedVersion, } = dataExplorerSlice.actions; export default dataExplorerSlice.reducer; From 775b8284fb298a830e5a1e3ba3c91a18d8c5e82d Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 26 Jun 2023 16:25:35 +0200 Subject: [PATCH 045/192] de-dn: use the relative path when switching between tabs in the editor and expaned toggle --- .../containers/ResourceViewContainer.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/shared/containers/ResourceViewContainer.tsx b/src/shared/containers/ResourceViewContainer.tsx index 85458bd26..4d12ee990 100644 --- a/src/shared/containers/ResourceViewContainer.tsx +++ b/src/shared/containers/ResourceViewContainer.tsx @@ -197,18 +197,24 @@ const ResourceViewContainer: React.FunctionComponent<{ const isLatest = latestResource?._rev === resource?._rev; const handleTabChange = (activeTabKey: string) => { - goToResource(orgLabel, projectLabel, resourceId, { - revision: resource ? resource._rev : undefined, - tab: activeTabKey, - }); + const newLink = `${location.pathname}${location.search}${activeTabKey}`; + history.push(newLink, location.state); + // goToResource(orgLabel, projectLabel, resourceId, { + // revision: resource ? resource._rev : undefined, + // tab: activeTabKey, + // }); }; const handleExpanded = (expanded: boolean) => { - goToResource(orgLabel, projectLabel, resourceId, { - expanded, - revision: resource ? resource._rev : undefined, - tab: activeTabKey, - }); + // goToResource(orgLabel, projectLabel, resourceId, { + // expanded, + // revision: resource ? resource._rev : undefined, + // tab: activeTabKey, + // }); + const searchParams = new URLSearchParams(location.search); + searchParams.set('expanded', expanded ? 'true' : 'false'); + const newLink = `${location.pathname}?${searchParams.toString()}`; + history.push(newLink, location.state); }; const handleEditFormSubmit = async (value: any) => { From 99a52a69a29d7cb44e6c642fb2f9a3dc6ee76514 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 27 Jun 2023 11:33:11 +0200 Subject: [PATCH 046/192] de-12: clear data-explorer navigation history only when change route form graph/flow --- .../DataExplorerGraphFlow/DateExplorerGraphFlow.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index fbdc89cd6..be3efbfc5 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -36,9 +36,11 @@ const DataExplorerGraphFlow = () => { }, [location.search, digestFirstRender.current]); useEffect(() => { - const unlisten = history.listen(() => { - dispatch(ResetDataExplorerGraphFlow({ initialState: null })); - localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + const unlisten = history.listen(location => { + if (!location.pathname.startsWith('/data-explorer/graph-flow')) { + dispatch(ResetDataExplorerGraphFlow({ initialState: null })); + localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + } }); return () => unlisten(); }, []); From 25a71bb0fb95db00bde97e9e21e405b4ffe7e62c Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 27 Jun 2023 11:34:00 +0200 Subject: [PATCH 047/192] de-dn: enable the code editor to take the screen hight when full screen limited version is enabled --- src/shared/components/ResourceEditor/CodeEditor.tsx | 6 +++++- src/shared/components/ResourceEditor/ResourceEditor.less | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index e23481907..0d8fcc203 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -4,6 +4,8 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { INDENT_UNIT } from '.'; import { clsx } from 'clsx'; import { Spin } from 'antd'; +import { useSelector } from 'react-redux'; +import { RootState } from 'shared/store/reducers'; type TCodeEditor = { busy: boolean; @@ -32,6 +34,7 @@ const CodeEditor = forwardRef( }, ref ) => { + const { limited } = useSelector((state: RootState) => state.dataExplorer); return ( ( } className={clsx( 'code-mirror-editor', - loadingResolution && 'resolution-on-progress' + loadingResolution && 'resolution-on-progress', + limited && 'full-screen-mode' )} onChange={handleChange} editorDidMount={editor => { diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 1ea19013b..984b1b33b 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -17,6 +17,11 @@ color: @text-color; } } +.resource-editor .full-screen-mode { + .CodeMirror { + height: calc(100vh - 200px) !important; + } +} .sm-string .cm-property { color: @text-color; From 78e910de98cc180492730552deed5822b1ccc421 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 27 Jun 2023 11:39:54 +0200 Subject: [PATCH 048/192] de-dn: isolate the code editor from the global store for clean test --- src/shared/components/ResourceEditor/CodeEditor.tsx | 10 +++++----- .../components/ResourceEditor/ResourceEditor.spec.tsx | 1 + src/shared/components/ResourceEditor/index.tsx | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 0d8fcc203..f6b656cd6 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -4,15 +4,14 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { INDENT_UNIT } from '.'; import { clsx } from 'clsx'; import { Spin } from 'antd'; -import { useSelector } from 'react-redux'; -import { RootState } from 'shared/store/reducers'; type TCodeEditor = { busy: boolean; value: string; editable: boolean; - keyFoldCode(cm: any): void; loadingResolution: boolean; + fullscreen: boolean; + keyFoldCode(cm: any): void; handleChange(editor: any, data: any, value: any): void; onLinkClick(_: any, ev: MouseEvent): void; onLinksFound(): void; @@ -20,12 +19,14 @@ type TCodeEditor = { type TEditorConfiguration = EditorConfiguration & { foldCode: boolean; }; + const CodeEditor = forwardRef( ( { busy, value, editable, + fullscreen, keyFoldCode, loadingResolution, handleChange, @@ -34,7 +35,6 @@ const CodeEditor = forwardRef( }, ref ) => { - const { limited } = useSelector((state: RootState) => state.dataExplorer); return ( ( className={clsx( 'code-mirror-editor', loadingResolution && 'resolution-on-progress', - limited && 'full-screen-mode' + fullscreen && 'full-screen-mode' )} onChange={handleChange} editorDidMount={editor => { diff --git a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx index 0162b0c0d..9041ab08e 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx +++ b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx @@ -36,6 +36,7 @@ describe('ResourceEditor', () => { handleChange={() => {}} loadingResolution={false} ref={editor} + fullscreen={false} /> ); await waitFor(async () => { diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index c69c12b8a..5b4d46a53 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -274,6 +274,7 @@ const ResourceEditor: React.FunctionComponent = props => { onLinkClick={() => {}} onLinksFound={() => {}} ref={codeMirorRef} + fullscreen={limited} />
); From 0580a9a428102f45dce5b39689cf82a22f74db0a Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 28 Jun 2023 13:07:04 +0200 Subject: [PATCH 049/192] de-12: remove the hash from the url when jump navigation --- .../NavigationStackItem.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx index c11fa5211..7b35bf562 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useHistory, useLocation } from 'react-router'; import { Tag, Tooltip } from 'antd'; import { clsx } from 'clsx'; import { isArray } from 'lodash'; @@ -29,10 +30,15 @@ const NavigationStackItem = ({ highlighted, }: TNavigationStackItem) => { const dispatch = useDispatch(); + const location = useLocation(); + const history = useHistory(); const { shrinked, links } = useSelector( (state: RootState) => state.dataExplorer ); - const onClick = () => dispatch(JumpToNodeDataExplorerGraphFlow(index)); + const onClick = () => { + dispatch(JumpToNodeDataExplorerGraphFlow(index)); + history.replace(location.pathname); + }; return (
Date: Tue, 27 Jun 2023 11:34:00 +0200 Subject: [PATCH 050/192] de-dn: enable the code editor to take the screen hight when full screen limited version is enabled --- src/shared/components/ResourceEditor/CodeEditor.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index f6b656cd6..53ff7a262 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -4,6 +4,8 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { INDENT_UNIT } from '.'; import { clsx } from 'clsx'; import { Spin } from 'antd'; +import { useSelector } from 'react-redux'; +import { RootState } from 'shared/store/reducers'; type TCodeEditor = { busy: boolean; @@ -35,6 +37,7 @@ const CodeEditor = forwardRef( }, ref ) => { + const { limited } = useSelector((state: RootState) => state.dataExplorer); return ( Date: Tue, 27 Jun 2023 11:39:54 +0200 Subject: [PATCH 051/192] de-dn: isolate the code editor from the global store for clean test --- src/shared/components/ResourceEditor/CodeEditor.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 53ff7a262..f6b656cd6 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -4,8 +4,6 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { INDENT_UNIT } from '.'; import { clsx } from 'clsx'; import { Spin } from 'antd'; -import { useSelector } from 'react-redux'; -import { RootState } from 'shared/store/reducers'; type TCodeEditor = { busy: boolean; @@ -37,7 +35,6 @@ const CodeEditor = forwardRef( }, ref ) => { - const { limited } = useSelector((state: RootState) => state.dataExplorer); return ( Date: Thu, 29 Jun 2023 10:56:59 +0200 Subject: [PATCH 052/192] de-12: clean code --- src/shared/containers/ResourceViewContainer.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/shared/containers/ResourceViewContainer.tsx b/src/shared/containers/ResourceViewContainer.tsx index 4d12ee990..95285d3d6 100644 --- a/src/shared/containers/ResourceViewContainer.tsx +++ b/src/shared/containers/ResourceViewContainer.tsx @@ -199,18 +199,8 @@ const ResourceViewContainer: React.FunctionComponent<{ const handleTabChange = (activeTabKey: string) => { const newLink = `${location.pathname}${location.search}${activeTabKey}`; history.push(newLink, location.state); - // goToResource(orgLabel, projectLabel, resourceId, { - // revision: resource ? resource._rev : undefined, - // tab: activeTabKey, - // }); }; - const handleExpanded = (expanded: boolean) => { - // goToResource(orgLabel, projectLabel, resourceId, { - // expanded, - // revision: resource ? resource._rev : undefined, - // tab: activeTabKey, - // }); const searchParams = new URLSearchParams(location.search); searchParams.set('expanded', expanded ? 'true' : 'false'); const newLink = `${location.pathname}?${searchParams.toString()}`; @@ -321,7 +311,6 @@ const ResourceViewContainer: React.FunctionComponent<{ projectLabel, resourceId )) as Resource; - const selectedResource: Resource = rev || tag ? ((await nexus.Resource.get( @@ -345,7 +334,6 @@ const ResourceViewContainer: React.FunctionComponent<{ )) as ExpandedResource[]; const expandedResource = expandedResources[0]; - setLatestResource(resource); setResource({ // Note: we must fetch the proper, expanded @id. The @id that comes from a normal request or from the URL From 9d3c766b93de0dfd66dc6ffac0e85f616d5f1627 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 29 Jun 2023 10:57:43 +0200 Subject: [PATCH 053/192] de-12: update tests for navigation-stack comp --- .../NavigationStack.spec.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index aa5341562..a891e1a45 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -9,7 +9,6 @@ import { Router } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import { NexusProvider } from '@bbp/react-nexus'; -import { waitFor } from '../../../utils/testUtil'; import { deltaPath } from '../../../__mocks__/handlers/handlers'; import NavigationStack from './NavigationStack'; import { From 91af6ce5f29c0c2480abd94877cc18bb2701eda5 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 29 Jun 2023 10:59:00 +0200 Subject: [PATCH 054/192] de-12: add handlers for data explorer test --- .../handlers/DataExplorer/handlers.ts | 761 ++++++++++++++++++ 1 file changed, 761 insertions(+) create mode 100644 src/__mocks__/handlers/DataExplorer/handlers.ts diff --git a/src/__mocks__/handlers/DataExplorer/handlers.ts b/src/__mocks__/handlers/DataExplorer/handlers.ts new file mode 100644 index 000000000..0da6082f3 --- /dev/null +++ b/src/__mocks__/handlers/DataExplorer/handlers.ts @@ -0,0 +1,761 @@ +import { rest } from 'msw'; +import { deltaPath } from '__mocks__/handlers/handlers'; + +const resource = { + '@context': [ + 'https://bluebrain.github.io/nexus/contexts/metadata.json', + 'https://bbp.neuroshapes.org', + ], + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/bfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', + '@type': ['Entity', 'Dataset', 'NeuronMorphology', 'ReconstructedCell'], + annotation: { + '@type': ['MTypeAnnotation', 'Annotation'], + hasBody: { + '@id': 'ilx:0383236', + '@type': ['MType', 'AnnotationBody'], + label: 'L6_SBC', + }, + name: 'M-type Annotation', + }, + brainLocation: { + '@type': 'BrainLocation', + brainRegion: { + '@id': 'uberon:0008933', + label: 'primary somatosensory cortex', + }, + layer: { + '@id': 'uberon:0005395', + label: 'layer 6', + }, + }, + contribution: [ + { + '@type': 'Contribution', + agent: { + '@id': 'https://orcid.org/0000-0001-9358-1315', + '@type': 'Agent', + }, + hadRole: { + '@id': 'Neuron:ElectrophysiologyRecordingRole', + label: 'neuron electrophysiology recording role', + }, + }, + { + '@type': 'Contribution', + agent: { + '@id': 'https://www.grid.ac/institutes/grid.5333.6', + '@type': 'Agent', + }, + }, + ], + derivation: { + '@type': 'Derivation', + entity: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/350bcafe-9cbb-4c15-bad3-1caed2cbb990', + '@type': ['PatchedCell', 'Entity'], + }, + }, + description: + 'This dataset is about an in vitro-filled neuron morphology from layer 6 with m-type L6_SBC. The distribution contains the neuron morphology in ASC and in SWC file format.', + distribution: [ + { + '@type': 'DataDownload', + atLocation: { + '@type': 'Location', + location: + 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/a/a/e/d/8/2/b/5/tkb060126a2_ch3_bc_n_jh_100x_1.asc', + store: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', + '@type': 'RemoteDiskStorage', + _rev: 1, + }, + }, + contentSize: { + unitCode: 'bytes', + value: 1097726, + }, + contentUrl: + 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2Fbf146eaf-48cf-4b83-b375-bbb92ce7f7c0', + digest: { + algorithm: 'SHA-256', + value: + 'efcf3d6660d9769b3f3066e874c8f13536fbc398b5605ffc5acc223884362ff6', + }, + encodingFormat: 'application/asc', + name: 'tkb060126a2_ch3_bc_n_jh_100x_1.asc', + }, + { + '@type': 'DataDownload', + atLocation: { + '@type': 'Location', + location: + 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/6/4/3/8/3/d/0/3/tkb060126a2_ch3_bc_n_jh_100x_1.swc', + store: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', + '@type': 'RemoteDiskStorage', + _rev: 1, + }, + }, + contentSize: { + unitCode: 'bytes', + value: 891821, + }, + contentUrl: + 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2F60025362-1ca8-425e-908c-a01e4661c3e7', + digest: { + algorithm: 'SHA-256', + value: + '22bac983b129fe806c80a9ddb4dcf77b79c1a6a28adffd6674290fb1f014a30e', + }, + encodingFormat: 'application/swc', + name: 'tkb060126a2_ch3_bc_n_jh_100x_1.swc', + }, + ], + generation: { + '@type': 'Generation', + activity: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/9ad281da-e352-4275-b1fa-6a3516a654c9', + '@type': ['Activity', 'Reconstruction'], + }, + }, + isPartOf: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/23d3d87e-94fe-4639-b5c8-a26a712587e6', + '@type': 'Entity', + }, + license: { + '@id': 'https://creativecommons.org/licenses/by/4.0/', + '@type': 'License', + }, + name: 'tkb060126a2_ch3_bc_n_jh_100x_1', + objectOfStudy: { + '@id': + 'http://bbp.epfl.ch/neurosciencegraph/taxonomies/objectsofstudy/singlecells', + '@type': 'ObjectOfStudy', + label: 'Single Cell', + }, + sameAs: + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/431a1196-47b5-41a2-931a-3577be9a2dc4', + subject: { + '@type': 'Subject', + species: { + '@id': 'NCBITaxon:10116', + label: 'Rattus norvegicus', + }, + }, + _constrainedBy: 'https://neuroshapes.org/dash/neuronmorphology', + _createdAt: '2021-11-23T11:34:00.952Z', + _createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/akkaufma', + _deprecated: false, + _incoming: + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf/incoming', + _outgoing: + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf/outgoing', + _project: 'https://bbp.epfl.ch/nexus/v1/projects/public/sscx', + _rev: 2, + _schemaProject: + 'https://bbp.epfl.ch/nexus/v1/projects/neurosciencegraph/datamodels', + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', + _updatedAt: '2023-06-23T07:34:56.011Z', + _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/cgonzale', +}; + +const initialResource = { + '@context': [ + 'https://bluebrain.github.io/nexus/contexts/metadata.json', + 'https://bbp.neuroshapes.org', + ], + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/bfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', + '@type': ['Entity', 'Dataset', 'NeuronMorphology', 'ReconstructedCell'], + annotation: { + '@type': ['MTypeAnnotation', 'Annotation'], + hasBody: { + '@id': 'ilx:0383236', + '@type': ['MType', 'AnnotationBody'], + label: 'L6_SBC', + }, + name: 'M-type Annotation', + }, + brainLocation: { + '@type': 'BrainLocation', + brainRegion: { + '@id': 'uberon:0008933', + label: 'primary somatosensory cortex', + }, + layer: { + '@id': 'uberon:0005395', + label: 'layer 6', + }, + }, + contribution: [ + { + '@type': 'Contribution', + agent: { + '@id': 'https://orcid.org/0000-0001-9358-1315', + '@type': 'Agent', + }, + hadRole: { + '@id': 'Neuron:ElectrophysiologyRecordingRole', + label: 'neuron electrophysiology recording role', + }, + }, + { + '@type': 'Contribution', + agent: { + '@id': 'https://www.grid.ac/institutes/grid.5333.6', + '@type': 'Agent', + }, + }, + ], + derivation: { + '@type': 'Derivation', + entity: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/350bcafe-9cbb-4c15-bad3-1caed2cbb990', + '@type': ['PatchedCell', 'Entity'], + }, + }, + description: + 'This dataset is about an in vitro-filled neuron morphology from layer 6 with m-type L6_SBC. The distribution contains the neuron morphology in ASC and in SWC file format.', + distribution: [ + { + '@type': 'DataDownload', + atLocation: { + '@type': 'Location', + location: + 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/a/a/e/d/8/2/b/5/tkb060126a2_ch3_bc_n_jh_100x_1.asc', + store: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', + '@type': 'RemoteDiskStorage', + _rev: 1, + }, + }, + contentSize: { + unitCode: 'bytes', + value: 1097726, + }, + contentUrl: + 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2Fbf146eaf-48cf-4b83-b375-bbb92ce7f7c0', + digest: { + algorithm: 'SHA-256', + value: + 'efcf3d6660d9769b3f3066e874c8f13536fbc398b5605ffc5acc223884362ff6', + }, + encodingFormat: 'application/asc', + name: 'tkb060126a2_ch3_bc_n_jh_100x_1.asc', + }, + { + '@type': 'DataDownload', + atLocation: { + '@type': 'Location', + location: + 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/6/4/3/8/3/d/0/3/tkb060126a2_ch3_bc_n_jh_100x_1.swc', + store: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', + '@type': 'RemoteDiskStorage', + _rev: 1, + }, + }, + contentSize: { + unitCode: 'bytes', + value: 891821, + }, + contentUrl: + 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2F60025362-1ca8-425e-908c-a01e4661c3e7', + digest: { + algorithm: 'SHA-256', + value: + '22bac983b129fe806c80a9ddb4dcf77b79c1a6a28adffd6674290fb1f014a30e', + }, + encodingFormat: 'application/swc', + name: 'tkb060126a2_ch3_bc_n_jh_100x_1.swc', + }, + ], + generation: { + '@type': 'Generation', + activity: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/9ad281da-e352-4275-b1fa-6a3516a654c9', + '@type': ['Activity', 'Reconstruction'], + }, + }, + isPartOf: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/23d3d87e-94fe-4639-b5c8-a26a712587e6', + '@type': 'Entity', + }, + license: { + '@id': 'https://creativecommons.org/licenses/by/4.0/', + '@type': 'License', + }, + name: 'tkb060126a2_ch3_bc_n_jh_100x_1', + objectOfStudy: { + '@id': + 'http://bbp.epfl.ch/neurosciencegraph/taxonomies/objectsofstudy/singlecells', + '@type': 'ObjectOfStudy', + label: 'Single Cell', + }, + sameAs: + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/431a1196-47b5-41a2-931a-3577be9a2dc4', + subject: { + '@type': 'Subject', + species: { + '@id': 'NCBITaxon:10116', + label: 'Rattus norvegicus', + }, + }, + _constrainedBy: 'https://neuroshapes.org/dash/neuronmorphology', + _createdAt: '2021-11-23T11:34:00.952Z', + _createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/akkaufma', + _deprecated: false, + _incoming: + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf/incoming', + _outgoing: + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf/outgoing', + _project: 'https://bbp.epfl.ch/nexus/v1/projects/public/sscx', + _rev: 2, + _schemaProject: + 'https://bbp.epfl.ch/nexus/v1/projects/neurosciencegraph/datamodels', + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', + _updatedAt: '2023-06-23T07:34:56.011Z', + _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/cgonzale', +}; +const initialResourceExpanded = { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/bfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', + '@type': [ + 'http://www.w3.org/ns/prov#Entity', + 'http://schema.org/Dataset', + 'https://neuroshapes.org/NeuronMorphology', + 'https://neuroshapes.org/ReconstructedCell', + ], + 'http://schema.org/description': [ + { + '@value': + 'This dataset is about an in vitro-filled neuron morphology from layer 6 with m-type L6_SBC. The distribution contains the neuron morphology in ASC and in SWC file format.', + }, + ], + 'http://schema.org/distribution': [ + { + '@type': ['http://schema.org/DataDownload'], + 'http://schema.org/contentSize': [ + { + 'http://schema.org/unitCode': [ + { + '@value': 'bytes', + }, + ], + 'http://schema.org/value': [ + { + '@value': 1097726, + }, + ], + }, + ], + 'http://schema.org/contentUrl': [ + { + '@id': + 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2Fbf146eaf-48cf-4b83-b375-bbb92ce7f7c0', + }, + ], + 'http://schema.org/encodingFormat': [ + { + '@value': 'application/asc', + }, + ], + 'http://schema.org/name': [ + { + '@value': 'tkb060126a2_ch3_bc_n_jh_100x_1.asc', + }, + ], + 'http://www.w3.org/ns/prov#atLocation': [ + { + '@type': ['http://www.w3.org/ns/prov#Location'], + 'https://neuroshapes.org/location': [ + { + '@value': + 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/a/a/e/d/8/2/b/5/tkb060126a2_ch3_bc_n_jh_100x_1.asc', + }, + ], + 'https://neuroshapes.org/store': [ + { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', + '@type': [ + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/_/RemoteDiskStorage', + ], + 'https://bluebrain.github.io/nexus/vocabulary/rev': [ + { + '@value': 1, + }, + ], + }, + ], + }, + ], + 'https://neuroshapes.org/digest': [ + { + 'http://schema.org/algorithm': [ + { + '@value': 'SHA-256', + }, + ], + 'http://schema.org/value': [ + { + '@value': + 'efcf3d6660d9769b3f3066e874c8f13536fbc398b5605ffc5acc223884362ff6', + }, + ], + }, + ], + }, + { + '@type': ['http://schema.org/DataDownload'], + 'http://schema.org/contentSize': [ + { + 'http://schema.org/unitCode': [ + { + '@value': 'bytes', + }, + ], + 'http://schema.org/value': [ + { + '@value': 891821, + }, + ], + }, + ], + 'http://schema.org/contentUrl': [ + { + '@id': + 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2F60025362-1ca8-425e-908c-a01e4661c3e7', + }, + ], + 'http://schema.org/encodingFormat': [ + { + '@value': 'application/swc', + }, + ], + 'http://schema.org/name': [ + { + '@value': 'tkb060126a2_ch3_bc_n_jh_100x_1.swc', + }, + ], + 'http://www.w3.org/ns/prov#atLocation': [ + { + '@type': ['http://www.w3.org/ns/prov#Location'], + 'https://neuroshapes.org/location': [ + { + '@value': + 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/6/4/3/8/3/d/0/3/tkb060126a2_ch3_bc_n_jh_100x_1.swc', + }, + ], + 'https://neuroshapes.org/store': [ + { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', + '@type': [ + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/_/RemoteDiskStorage', + ], + 'https://bluebrain.github.io/nexus/vocabulary/rev': [ + { + '@value': 1, + }, + ], + }, + ], + }, + ], + 'https://neuroshapes.org/digest': [ + { + 'http://schema.org/algorithm': [ + { + '@value': 'SHA-256', + }, + ], + 'http://schema.org/value': [ + { + '@value': + '22bac983b129fe806c80a9ddb4dcf77b79c1a6a28adffd6674290fb1f014a30e', + }, + ], + }, + ], + }, + ], + 'http://schema.org/isPartOf': [ + { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/23d3d87e-94fe-4639-b5c8-a26a712587e6', + '@type': ['http://www.w3.org/ns/prov#Entity'], + }, + ], + 'http://schema.org/license': [ + { + '@id': 'https://creativecommons.org/licenses/by/4.0/', + '@type': ['https://neuroshapes.org/License'], + }, + ], + 'http://schema.org/name': [ + { + '@value': 'tkb060126a2_ch3_bc_n_jh_100x_1', + }, + ], + 'http://schema.org/sameAs': [ + { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/431a1196-47b5-41a2-931a-3577be9a2dc4', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/constrainedBy': [ + { + '@id': 'https://neuroshapes.org/dash/neuronmorphology', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/createdAt': [ + { + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + '@value': '2021-11-23T11:34:00.952Z', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/createdBy': [ + { + '@id': 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/akkaufma', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/deprecated': [ + { + '@value': false, + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/incoming': [ + { + '@id': + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf/incoming', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/outgoing': [ + { + '@id': + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf/outgoing', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/project': [ + { + '@id': 'https://bbp.epfl.ch/nexus/v1/projects/public/sscx', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/rev': [ + { + '@value': 2, + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/schemaProject': [ + { + '@id': + 'https://bbp.epfl.ch/nexus/v1/projects/neurosciencegraph/datamodels', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/self': [ + { + '@id': + 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/updatedAt': [ + { + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + '@value': '2023-06-23T07:34:56.011Z', + }, + ], + 'https://bluebrain.github.io/nexus/vocabulary/updatedBy': [ + { + '@id': 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/cgonzale', + }, + ], + 'https://neuroshapes.org/annotation': [ + { + '@type': [ + 'https://neuroshapes.org/MTypeAnnotation', + 'https://neuroshapes.org/Annotation', + ], + 'http://schema.org/name': [ + { + '@value': 'M-type Annotation', + }, + ], + 'https://neuroshapes.org/hasBody': [ + { + '@id': 'http://uri.interlex.org/base/ilx_0383236', + '@type': [ + 'https://neuroshapes.org/MType', + 'https://neuroshapes.org/AnnotationBody', + ], + 'http://www.w3.org/2000/01/rdf-schema#label': [ + { + '@value': 'L6_SBC', + }, + ], + }, + ], + }, + ], + 'https://neuroshapes.org/brainLocation': [ + { + '@type': ['https://neuroshapes.org/BrainLocation'], + 'https://neuroshapes.org/brainRegion': [ + { + '@id': 'http://purl.obolibrary.org/obo/UBERON_0008933', + 'http://www.w3.org/2000/01/rdf-schema#label': [ + { + '@value': 'primary somatosensory cortex', + }, + ], + }, + ], + 'https://neuroshapes.org/layer': [ + { + '@id': 'http://purl.obolibrary.org/obo/UBERON_0005395', + 'http://www.w3.org/2000/01/rdf-schema#label': [ + { + '@value': 'layer 6', + }, + ], + }, + ], + }, + ], + 'https://neuroshapes.org/contribution': [ + { + '@type': ['https://neuroshapes.org/Contribution'], + 'http://www.w3.org/ns/prov#agent': [ + { + '@id': 'https://orcid.org/0000-0001-9358-1315', + '@type': ['http://www.w3.org/ns/prov#Agent'], + }, + ], + 'http://www.w3.org/ns/prov#hadRole': [ + { + '@id': 'https://neuroshapes.org/NeuronElectrophysiologyRecordingRole', + 'http://www.w3.org/2000/01/rdf-schema#label': [ + { + '@value': 'neuron electrophysiology recording role', + }, + ], + }, + ], + }, + { + '@type': ['https://neuroshapes.org/Contribution'], + 'http://www.w3.org/ns/prov#agent': [ + { + '@id': 'https://www.grid.ac/institutes/grid.5333.6', + '@type': ['http://www.w3.org/ns/prov#Agent'], + }, + ], + }, + ], + 'https://neuroshapes.org/derivation': [ + { + '@type': ['http://www.w3.org/ns/prov#Derivation'], + 'http://www.w3.org/ns/prov#entity': [ + { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/350bcafe-9cbb-4c15-bad3-1caed2cbb990', + '@type': [ + 'https://neuroshapes.org/PatchedCell', + 'http://www.w3.org/ns/prov#Entity', + ], + }, + ], + }, + ], + 'https://neuroshapes.org/generation': [ + { + '@type': ['http://www.w3.org/ns/prov#Generation'], + 'http://www.w3.org/ns/prov#activity': [ + { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/9ad281da-e352-4275-b1fa-6a3516a654c9', + '@type': [ + 'http://www.w3.org/ns/prov#Activity', + 'https://neuroshapes.org/Reconstruction', + ], + }, + ], + }, + ], + 'https://neuroshapes.org/objectOfStudy': [ + { + '@id': + 'http://bbp.epfl.ch/neurosciencegraph/taxonomies/objectsofstudy/singlecells', + '@type': ['https://neuroshapes.org/ObjectOfStudy'], + 'http://www.w3.org/2000/01/rdf-schema#label': [ + { + '@value': 'Single Cell', + }, + ], + }, + ], + 'https://neuroshapes.org/subject': [ + { + '@type': ['https://neuroshapes.org/Subject'], + 'https://neuroshapes.org/species': [ + { + '@id': 'http://purl.obolibrary.org/obo/NCBITaxon_10116', + 'http://www.w3.org/2000/01/rdf-schema#label': [ + { + '@value': 'Rattus norvegicus', + }, + ], + }, + ], + }, + ], +}; + +const getDataExplorerGraphFlowResourceObject = rest.get( + deltaPath( + `resources/public/sscx/_/${encodeURIComponent( + initialResource['@id'] + )}?format=expanded` + ), + (req, res, ctx) => { + const format = req.url.searchParams.get('format'); + if (format === 'expanded') { + return res(ctx.status(200), ctx.json([initialResourceExpanded])); + } + return res(ctx.status(200), ctx.json(initialResource)); + } +); +const getDataExplorerGraphFlowResourceObjectTags = rest.get( + deltaPath( + `resources/public/sscx/_/${encodeURIComponent(initialResource['@id'])}/tags` + ), + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + '@context': 'https://bluebrain.github.io/nexus/contexts/tags.json', + tags: [], + }) + ); + } +); + +export { + resource, + initialResource, + getDataExplorerGraphFlowResourceObject, + getDataExplorerGraphFlowResourceObjectTags, +}; From d178adc48ef40e392b15ad47f299f0a9eddab883 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 29 Jun 2023 11:00:56 +0200 Subject: [PATCH 055/192] de-12: add test for data explorer page --- .../DateExplorerGraphFlow.spec.tsx | 135 ++++++++++++++++++ .../ResourceEditor/ResourceEditor.spec.tsx | 2 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx new file mode 100644 index 000000000..92c6d4ca2 --- /dev/null +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx @@ -0,0 +1,135 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { NexusClient, createNexusClient } from '@bbp/nexus-sdk'; +import { AnyAction, Store } from 'redux'; +import { NexusProvider } from '@bbp/react-nexus'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; +import { setupServer } from 'msw/node'; +import { deltaPath } from '__mocks__/handlers/handlers'; +import { cleanup, render, act, screen } from '../../../utils/testUtil'; +import { + DATA_EXPLORER_GRAPH_FLOW_DIGEST, + InitNewVisitDataExplorerGraphView, +} from '../../../shared/store/reducers/data-explorer'; +import configureStore from '../../store'; +import DateExplorerGraphFlow from './DateExplorerGraphFlow'; +import { + initialResource, + getDataExplorerGraphFlowResourceObject, + getDataExplorerGraphFlowResourceObjectTags, +} from '../../../__mocks__/handlers/DataExplorer/handlers'; + +const initialDataExplorerState = { + current: { + isDownloadable: false, + _self: initialResource._self, + title: initialResource.name, + types: initialResource['@type'], + resource: ['public', 'sscx', initialResource['@id'], initialResource._rev], + }, + links: [], + shrinked: false, + highlightIndex: -1, + limited: false, +}; +describe('DataExplorerGraphFlow', () => { + let store: Store; + let history: MemoryHistory<{}>; + let server: ReturnType; + let nexus: NexusClient; + beforeAll(async () => { + nexus = createNexusClient({ + fetch, + uri: deltaPath(), + }); + server = setupServer( + getDataExplorerGraphFlowResourceObject, + getDataExplorerGraphFlowResourceObjectTags + ); + + server.listen(); + history = createMemoryHistory({}); + store = configureStore( + history, + { nexus }, + { + router: { + location: { + pathname: '/', + search: '', + hash: '', + state: {}, + key: 'cvvg7m', + query: {}, + }, + action: 'POP', + }, + } + ); + }); + + afterAll(() => { + server.resetHandlers(); + server.close(); + localStorage.clear(); + cleanup(); + }); + + it('should render the name of the resource', async () => { + const App: JSX.Element = ( + + + + + + + + ); + await act(async () => { + await render(App); + }); + store.dispatch( + InitNewVisitDataExplorerGraphView({ + current: initialDataExplorerState.current, + limited: false, + }) + ); + + const resourceTitle = await waitFor(() => + screen.getByText(initialResource.name) + ); + expect(resourceTitle).toBeInTheDocument(); + }); + it('should clean the data explorer state when quit the page', async () => { + const App: JSX.Element = ( + + + + + + + + ); + await act(async () => { + await render(App); + }); + store.dispatch( + InitNewVisitDataExplorerGraphView({ + current: initialDataExplorerState.current, + limited: false, + }) + ); + history.push('/another-page'); + const dataExplorerState = store.getState().dataExplorer; + const localstorage = localStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + expect(localstorage).toBeNull(); + expect(dataExplorerState.links.length).toBe(0); + expect(dataExplorerState.current).toBeNull(); + expect(dataExplorerState.shrinked).toBe(false); + expect(dataExplorerState.limited).toBe(false); + expect(dataExplorerState.highlightIndex).toBe(-1); + }); +}); diff --git a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx index 9041ab08e..d78c691df 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx +++ b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx @@ -24,7 +24,7 @@ describe('ResourceEditor', () => { it('check if code editor will be rendered in the screen', async () => { const editor = React.createRef(); const onLinksFound = jest.fn(); - const { queryByText, container, getByTestId } = render( + const { queryByText, container } = render( Date: Fri, 30 Jun 2023 09:03:00 +0200 Subject: [PATCH 056/192] de-12: add page when no current for graph-flow --- .../DateExplorerGraphFlow.tsx | 20 ++++++++++++- .../canvas/DataExplorerGraphFlow/styles.less | 29 +++++++++++++++++++ src/shared/images/graphNodes.svg | 1 + 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/shared/images/graphNodes.svg diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index be3efbfc5..18f1ab33d 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -21,7 +21,7 @@ const DataExplorerGraphFlow = () => { const location = useLocation(); const dispatch = useDispatch(); const digestFirstRender = useRef(false); - const { links, shrinked } = useSelector( + const { links, shrinked, current } = useSelector( (state: RootState) => state.dataExplorer ); @@ -45,6 +45,24 @@ const DataExplorerGraphFlow = () => { return () => unlisten(); }, []); + if (current === null) { + return ( +
+
+ nodes +
No data explorer graph flow
+
+ Please select a node from any resource view editor to start + exploring +
+
+
+ ); + } return (
15191549165518251716144116501668154716051636151214891591185716061840152517671516143115451430191116951683168412691521165616821671155116691754170015231537153815261524154116761635162515041568145915791617174714951665159314401698152817111697167015741559176116881443170615501531179114581498155518451563159216421493159518631501146716541757156217491707176415691742171218421439 \ No newline at end of file From 9a6384eaeea17f603d4bfb283bb6132567c03cc1 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 30 Jun 2023 17:12:08 +0200 Subject: [PATCH 057/192] de-12: fix rebase conflicts --- .../components/ResourceEditor/editorUtils.ts | 55 ++++++++++++++----- .../components/ResourceEditor/index.tsx | 6 -- src/shared/store/reducers/data-explorer.ts | 2 +- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index d4e6b5818..ad76da8c6 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -1,4 +1,4 @@ -import { NexusClient } from '@bbp/nexus-sdk'; +import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { Dispatch } from 'redux'; import { isArray, last } from 'lodash'; import isValidUrl, { externalLink } from '../../../utils/validUrl'; @@ -35,6 +35,13 @@ const dispatchEvent = ( payload: data.payload, }); }; + +const isDownloadableLink = (resource: Resource) => { + return Boolean( + resource['@type'] === 'File' || resource['@type']?.includes('File') + ); +}; + export const getNormalizedTypes = (types?: string | string[]) => { if (types) { if (isArray(types)) { @@ -76,6 +83,7 @@ export async function resolveLinkInEditor({ resourceId: encodeURIComponent(url), }); const entity = getOrgAndProjectFromResourceObject(data); + const isDownloadable = isDownloadableLink(data); return dispatchEvent(dispatch, { type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, payload: { @@ -83,15 +91,24 @@ export async function resolveLinkInEditor({ error: null, resolvedAs: 'resource', results: { + isDownloadable, _self: data._self, title: getResourceLabel(data), types: getNormalizedTypes(data['@type']), - resource: [ - entity?.orgLabel, - entity?.projectLabel, - data['@id'], - data._rev, - ], + resource: isDownloadable + ? [ + entity?.orgLabel, + entity?.projectLabel, + data['@id'], + data._rev, + data._mediaType, + ] + : [ + entity?.orgLabel, + entity?.projectLabel, + data['@id'], + data._rev, + ], }, }, }); @@ -115,27 +132,38 @@ export async function resolveLinkInEditor({ _self: url, title: url, types: [], + isDownloadable: false, }, } : !data._total ? { error: 'No @id or _self has been resolved', resolvedAs: 'error', + isDownloadable: false, } : { resolvedAs: 'resources', results: data._results.map(item => { + const isDownloadable = isDownloadableLink(item); const entity = getOrgAndProjectFromResourceObject(item); return { _self: item._self, title: getResourceLabel(item), types: getNormalizedTypes(item['@type']), - resource: [ - entity?.orgLabel, - entity?.projectLabel, - item['@id'], - item._rev, - ], + resource: isDownloadable + ? [ + entity?.orgLabel, + entity?.projectLabel, + item['@id'], + item._rev, + item._mediaType, + ] + : [ + entity?.orgLabel, + entity?.projectLabel, + item['@id'], + item._rev, + ], }; }), }), @@ -154,6 +182,7 @@ export async function resolveLinkInEditor({ _self: url, title: url, types: [], + isDownloadable: false, }, }, }); diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 5b4d46a53..ebcbf1801 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -44,12 +44,6 @@ export const LINE_HEIGHT = 50; export const INDENT_UNIT = 4; const switchMarginRight = { marginRight: 5 }; -const isDownloadableLink = (resource: Resource) => { - return Boolean( - resource['@type'] === 'File' || resource['@type']?.includes('File') - ); -}; - const ResourceEditor: React.FunctionComponent = props => { const { rawData, diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index c68a9e29a..83c0bb90b 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -16,7 +16,7 @@ export type TDELink = { title: string; types?: string | string[]; resource?: TDEResource; - isDownloadable: boolean; + isDownloadable?: boolean; }; export type TDataExplorerState = { links: TDELink[]; From f7172bb15d489cbb1e28913edcfa31e2cb010397 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 27 Jun 2023 11:34:00 +0200 Subject: [PATCH 058/192] de-12: enable the code editor to take the screen hight when full screen limited version is enabled --- src/shared/components/ResourceEditor/CodeEditor.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index f6b656cd6..53ff7a262 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -4,6 +4,8 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { INDENT_UNIT } from '.'; import { clsx } from 'clsx'; import { Spin } from 'antd'; +import { useSelector } from 'react-redux'; +import { RootState } from 'shared/store/reducers'; type TCodeEditor = { busy: boolean; @@ -35,6 +37,7 @@ const CodeEditor = forwardRef( }, ref ) => { + const { limited } = useSelector((state: RootState) => state.dataExplorer); return ( Date: Tue, 27 Jun 2023 11:39:54 +0200 Subject: [PATCH 059/192] de-dn: isolate the code editor from the global store for clean test --- src/shared/components/ResourceEditor/CodeEditor.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 53ff7a262..f6b656cd6 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -4,8 +4,6 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { INDENT_UNIT } from '.'; import { clsx } from 'clsx'; import { Spin } from 'antd'; -import { useSelector } from 'react-redux'; -import { RootState } from 'shared/store/reducers'; type TCodeEditor = { busy: boolean; @@ -37,7 +35,6 @@ const CodeEditor = forwardRef( }, ref ) => { - const { limited } = useSelector((state: RootState) => state.dataExplorer); return ( Date: Tue, 27 Jun 2023 11:34:00 +0200 Subject: [PATCH 060/192] de-dn: enable the code editor to take the screen hight when full screen limited version is enabled --- src/shared/components/ResourceEditor/CodeEditor.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index f6b656cd6..53ff7a262 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -4,6 +4,8 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { INDENT_UNIT } from '.'; import { clsx } from 'clsx'; import { Spin } from 'antd'; +import { useSelector } from 'react-redux'; +import { RootState } from 'shared/store/reducers'; type TCodeEditor = { busy: boolean; @@ -35,6 +37,7 @@ const CodeEditor = forwardRef( }, ref ) => { + const { limited } = useSelector((state: RootState) => state.dataExplorer); return ( Date: Tue, 27 Jun 2023 11:39:54 +0200 Subject: [PATCH 061/192] de-dn: isolate the code editor from the global store for clean test --- src/shared/components/ResourceEditor/CodeEditor.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 53ff7a262..f6b656cd6 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -4,8 +4,6 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { INDENT_UNIT } from '.'; import { clsx } from 'clsx'; import { Spin } from 'antd'; -import { useSelector } from 'react-redux'; -import { RootState } from 'shared/store/reducers'; type TCodeEditor = { busy: boolean; @@ -37,7 +35,6 @@ const CodeEditor = forwardRef( }, ref ) => { - const { limited } = useSelector((state: RootState) => state.dataExplorer); return ( Date: Tue, 4 Jul 2023 13:07:29 +0200 Subject: [PATCH 062/192] fix: styles and navigation url --- .../canvas/DataExplorerGraphFlow/styles.less | 1 + .../components/ResourceEditor/index.tsx | 4 ++-- .../NavigationBack.tsx | 22 ++++++++++--------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/styles.less b/src/shared/canvas/DataExplorerGraphFlow/styles.less index f2a5b0a41..c1543b8b2 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/styles.less +++ b/src/shared/canvas/DataExplorerGraphFlow/styles.less @@ -37,6 +37,7 @@ align-items: flex-start; justify-content: space-between; gap: 10px; + margin-top: 52px; } &.no-current { diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index ebcbf1801..fbe4d9ed9 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -265,8 +265,8 @@ const ResourceEditor: React.FunctionComponent = props => { handleChange={handleChange} keyFoldCode={keyFoldCode} loadingResolution={loadingResolution} - onLinkClick={() => {}} - onLinksFound={() => {}} + onLinkClick={onLinkClick} + onLinksFound={onLinksFound} ref={codeMirorRef} fullscreen={limited} /> diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx index 86531a53e..a36b047d9 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; +import { useHistory } from 'react-router'; import { ArrowLeftOutlined } from '@ant-design/icons'; import { ReturnBackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; @@ -7,17 +8,18 @@ import './styles.less'; const NavigationBack = () => { const dispatch = useDispatch(); + const history = useHistory(); const { links } = useSelector((state: RootState) => state.dataExplorer); - const onBack = () => dispatch(ReturnBackDataExplorerGraphFlow()); - if (links.length) { - return ( - - ); - } - return null; + const onBack = () => { + history.replace(location.pathname); + dispatch(ReturnBackDataExplorerGraphFlow()); + }; + return links.length ? ( + + ) : null; }; export default NavigationBack; From db31652661b09ab70df33b69ae07932d11d89018 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 4 Jul 2023 14:30:36 +0200 Subject: [PATCH 063/192] de-12: add loading state to download action --- .../components/ResourceEditor/editorUtils.ts | 1 + .../ResolvedLinkEditorPopover.tsx | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index ad76da8c6..3b613054a 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -147,6 +147,7 @@ export async function resolveLinkInEditor({ const isDownloadable = isDownloadableLink(item); const entity = getOrgAndProjectFromResourceObject(item); return { + isDownloadable, _self: item._self, title: getResourceLabel(item), types: getNormalizedTypes(item['@type']), diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx index a5e5cc8c8..473f7300b 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx @@ -1,11 +1,12 @@ import React, { ReactNode, useRef, forwardRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; +import { useMutation } from 'react-query'; import { useHistory, useLocation, useRouteMatch } from 'react-router'; import { useNexusContext } from '@bbp/react-nexus'; import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { clsx } from 'clsx'; -import { Tag, Divider } from 'antd'; -import { DownloadOutlined } from '@ant-design/icons'; +import { Tag } from 'antd'; +import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; import { match as pmatch } from 'ts-pattern'; import { UISettingsActionTypes } from '../../store/actions/ui-settings'; import { RootState } from '../../store/reducers'; @@ -136,7 +137,7 @@ const ResolvedLinkEditorPopover = () => { } }; - const onDownload = async (data: TDELink) => { + const handleDownloadBinary = async (data: TDELink) => { await downloadFile({ nexus, orgLabel: data.resource?.[0]!, @@ -147,6 +148,11 @@ const ResolvedLinkEditorPopover = () => { }); }; + const { + mutateAsync: downloadBinaryAsync, + isLoading: downloadInProgress, + } = useMutation([], handleDownloadBinary); + return pmatch(resultPattern) .with({ open: true, resolvedAs: 'error' }, () => ( @@ -169,7 +175,13 @@ const ResolvedLinkEditorPopover = () => { {result.isDownloadable && (
- onDownload(result)} /> + {downloadInProgress ? ( + + ) : ( + downloadBinaryAsync(result)} + /> + )}
)}
@@ -192,7 +204,13 @@ const ResolvedLinkEditorPopover = () => { {item.isDownloadable && (
- onDownload(item)} /> + {downloadInProgress ? ( + + ) : ( + downloadBinaryAsync(item)} + /> + )}
)}
From 1894d76e4a672cc383215b4018549e4e22738324 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 4 Jul 2023 15:37:33 +0200 Subject: [PATCH 064/192] de-12: fix external links de-12: fix some styles de-12: add storage link validator --- .../components/ResourceEditor/editorUtils.ts | 104 +++++++++--------- .../components/ResourceEditor/index.tsx | 9 +- .../ResolvedLinkEditorPopover.tsx | 23 ++-- .../ResolvedLinkEditorPopover/styles.less | 23 +++- src/utils/validUrl.ts | 9 +- 5 files changed, 99 insertions(+), 69 deletions(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index 3b613054a..cf3f26de6 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -1,7 +1,12 @@ +import { extractFieldName } from './../../../subapps/search/containers/FilterOptions'; import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { Dispatch } from 'redux'; -import { isArray, last } from 'lodash'; -import isValidUrl, { externalLink } from '../../../utils/validUrl'; +import { has, isArray, last } from 'lodash'; +import isValidUrl, { + isExternalLink, + isStorageLink, + isUrlCurieFormat, +} from '../../../utils/validUrl'; import { fetchResourceByResolver } from '../../../subapps/admin/components/Settings/ResolversSubView'; import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; import { @@ -23,6 +28,13 @@ type TActionData = { payload: TEditorPopoverResolvedData; }; +type TDeltaError = Error & { + '@type': string; + details: string; +}; +type TErrorMessage = Error & { + message: string; +}; const dispatchEvent = ( dispatch: Dispatch, data: TActionData @@ -57,6 +69,10 @@ export const getNormalizedTypes = (types?: string | string[]) => { return []; }; +const mayBeResolvableLink = (url: string): boolean => { + return isValidUrl(url) && !isUrlCurieFormat(url) && !isStorageLink(url); +}; + export async function resolveLinkInEditor({ nexus, dispatch, @@ -72,7 +88,7 @@ export async function resolveLinkInEditor({ projectLabel: string; defaultPaylaod: { top: number; left: number; open: boolean }; }) { - if (isValidUrl(url)) { + if (mayBeResolvableLink(url)) { let data; try { // case-1: link resolved by the project resolver @@ -121,59 +137,47 @@ export async function resolveLinkInEditor({ data = await nexus.Resource.list(undefined, undefined, { locate: url, }); + if ( + !data._total || + (!data._total && url.startsWith('https://bbp.epfl.ch')) + ) { + throw new Error('Resource can not be resolved'); + } return dispatchEvent(dispatch, { type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, payload: { ...defaultPaylaod, - ...(externalLink(url) && !data._total - ? { - resolvedAs: 'external', - results: { - _self: url, - title: url, - types: [], - isDownloadable: false, - }, - } - : !data._total - ? { - error: 'No @id or _self has been resolved', - resolvedAs: 'error', - isDownloadable: false, - } - : { - resolvedAs: 'resources', - results: data._results.map(item => { - const isDownloadable = isDownloadableLink(item); - const entity = getOrgAndProjectFromResourceObject(item); - return { - isDownloadable, - _self: item._self, - title: getResourceLabel(item), - types: getNormalizedTypes(item['@type']), - resource: isDownloadable - ? [ - entity?.orgLabel, - entity?.projectLabel, - item['@id'], - item._rev, - item._mediaType, - ] - : [ - entity?.orgLabel, - entity?.projectLabel, - item['@id'], - item._rev, - ], - }; - }), - }), + resolvedAs: 'resources', + results: data._results.map(item => { + const isDownloadable = isDownloadableLink(item); + const entity = getOrgAndProjectFromResourceObject(item); + return { + isDownloadable, + _self: item._self, + title: getResourceLabel(item), + types: getNormalizedTypes(item['@type']), + resource: isDownloadable + ? [ + entity?.orgLabel, + entity?.projectLabel, + item['@id'], + item._rev, + item._mediaType, + ] + : [ + entity?.orgLabel, + entity?.projectLabel, + item['@id'], + item._rev, + ], + }; + }), }, }); } catch (error) { // case-3: if an error occured when tring both resolution method above // we check if the resource is external - if (externalLink(url)) { + if (isExternalLink(url)) { return dispatchEvent(dispatch, { type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, payload: { @@ -183,18 +187,18 @@ export async function resolveLinkInEditor({ _self: url, title: url, types: [], - isDownloadable: false, }, }, }); } - // case-4: if not an external url then it will be an error return dispatchEvent(dispatch, { type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, payload: { ...defaultPaylaod, - error: JSON.stringify(error), + error: has(error, 'details') + ? (error as TDeltaError).details + : (error as TErrorMessage).message ?? JSON.stringify(error), resolvedAs: 'error', }, }); diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index fbe4d9ed9..73d95bcc6 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -14,10 +14,12 @@ import 'codemirror/mode/javascript/javascript'; import 'codemirror/addon/fold/foldcode'; import 'codemirror/addon/fold/foldgutter'; import 'codemirror/addon/fold/brace-fold'; -import isValidUrl from '../../../utils/validUrl'; +import isValidUrl, { + isStorageLink, + isUrlCurieFormat, +} from '../../../utils/validUrl'; import CodeEditor from './CodeEditor'; import { TToken, resolveLinkInEditor } from './editorUtils'; -import { Resource } from '@bbp/nexus-sdk'; import { RootState } from '../../store/reducers'; import './ResourceEditor.less'; @@ -112,7 +114,8 @@ const ResourceEditor: React.FunctionComponent = props => { const elements = document.getElementsByClassName('cm-string'); Array.from(elements).forEach(item => { const itemSpan = item as HTMLSpanElement; - if (isValidUrl(itemSpan.innerText.replace(/^"|"$/g, ''))) { + const url = itemSpan.innerText.replace(/^"|"$/g, ''); + if (isValidUrl(url) && !isUrlCurieFormat(url) && !isStorageLink(url)) { itemSpan.style.textDecoration = 'underline'; } }); diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx index 473f7300b..841bce1ab 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx @@ -5,7 +5,7 @@ import { useHistory, useLocation, useRouteMatch } from 'react-router'; import { useNexusContext } from '@bbp/react-nexus'; import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { clsx } from 'clsx'; -import { Tag } from 'antd'; +import { Button, Tag } from 'antd'; import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; import { match as pmatch } from 'ts-pattern'; import { UISettingsActionTypes } from '../../store/actions/ui-settings'; @@ -156,7 +156,10 @@ const ResolvedLinkEditorPopover = () => { return pmatch(resultPattern) .with({ open: true, resolvedAs: 'error' }, () => ( -
{error}
+
+ Error +
{error}
+
)) .with({ open: true, resolvedAs: 'resource' }, () => { @@ -224,13 +227,15 @@ const ResolvedLinkEditorPopover = () => {
External Link - - This is external Link please configure CrossProjectResolver for - your project - - + + {result.title} +
); diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less index 447ccdb77..9931c3a25 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less +++ b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less @@ -23,6 +23,8 @@ overflow: hidden; span.ant-tag { margin: 10px 12px; + border-radius: 4px; + box-shadow: 0 2px 12px rgba(#333, 0.12); } &.list-item { @@ -33,7 +35,7 @@ } &:last-child { span.ant-tag { - margin: 5px 12px 10px; + margin: 10px 12px 10px; } } span.ant-tag { @@ -68,16 +70,29 @@ } .external { - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; + flex-direction: row; + align-items: center; + justify-content: center; max-width: inherit; + gap: 10px; padding: 10px; + span.ant-tag { + margin: 0; + } + .popover-btn.link { + padding: 0; + } } &.error { background-color: rgba(255, 0, 0, 0.478); color: white; + span.ant-tag { + margin-right: 0px; + } + .popover-btn.error { + word-break: break-all; + } } .popover-download-btn { diff --git a/src/utils/validUrl.ts b/src/utils/validUrl.ts index d446d37e2..339b1480d 100644 --- a/src/utils/validUrl.ts +++ b/src/utils/validUrl.ts @@ -27,9 +27,12 @@ function isUrlCurieFormat(str: string) { return curiePattern.test(str); } -function externalLink(url: string): boolean { - return !url.startsWith('https://') && !url.startsWith('http://'); +function isExternalLink(url: string): boolean { + return !url.startsWith('https://bbp.epfl.ch'); } -export { easyValidURL, isUrlCurieFormat, externalLink }; +function isStorageLink(url: string): boolean { + return url.startsWith('file:///gpfs'); +} +export { easyValidURL, isUrlCurieFormat, isExternalLink, isStorageLink }; export default isValidUrl; From 84699768adfd4211a15dafbef6054259d63719be Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 4 Jul 2023 15:55:59 +0200 Subject: [PATCH 065/192] de-12: refactor checking if a line in the editor is clickable --- src/shared/components/ResourceEditor/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 73d95bcc6..4784b34a9 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -46,6 +46,10 @@ export const LINE_HEIGHT = 50; export const INDENT_UNIT = 4; const switchMarginRight = { marginRight: 5 }; +const isClickableLine = (url: string) => { + return isValidUrl(url) && !isUrlCurieFormat(url) && !isStorageLink(url); +}; + const ResourceEditor: React.FunctionComponent = props => { const { rawData, @@ -115,7 +119,7 @@ const ResourceEditor: React.FunctionComponent = props => { Array.from(elements).forEach(item => { const itemSpan = item as HTMLSpanElement; const url = itemSpan.innerText.replace(/^"|"$/g, ''); - if (isValidUrl(url) && !isUrlCurieFormat(url) && !isStorageLink(url)) { + if (isClickableLine(url)) { itemSpan.style.textDecoration = 'underline'; } }); From 75a772b3746d89dcea7150716da9a8e115b504e9 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 20 Jun 2023 11:57:11 +0200 Subject: [PATCH 066/192] [f-3983] add advanced mode toggle --- src/shared/components/Header/Header.less | 1 - src/shared/components/Header/Header.tsx | 4 +- src/shared/lib.less | 1 + .../AdvancedMode/AdvancedMode.spec.tsx | 93 +++++++++++++++++++ .../molecules/AdvancedMode/AdvancedMode.tsx | 50 ++++++++++ src/shared/molecules/AdvancedMode/styles.less | 47 ++++++++++ src/shared/molecules/index.ts | 1 + src/shared/store/actions/ui-settings.ts | 1 + .../store/reducers/__tests__/ui-settings.ts | 1 + src/shared/store/reducers/ui-settings.ts | 8 ++ 10 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/shared/molecules/AdvancedMode/AdvancedMode.spec.tsx create mode 100644 src/shared/molecules/AdvancedMode/AdvancedMode.tsx create mode 100644 src/shared/molecules/AdvancedMode/styles.less diff --git a/src/shared/components/Header/Header.less b/src/shared/components/Header/Header.less index d72259d80..9b6e0dd3e 100644 --- a/src/shared/components/Header/Header.less +++ b/src/shared/components/Header/Header.less @@ -29,7 +29,6 @@ // padding-right: @default-pad; align-items: center; min-width: 120px; - button, a { // margin: 0 4px; diff --git a/src/shared/components/Header/Header.tsx b/src/shared/components/Header/Header.tsx index e1a53153c..ae4eea366 100644 --- a/src/shared/components/Header/Header.tsx +++ b/src/shared/components/Header/Header.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useState } from 'react'; +import React from 'react'; import { Link } from 'react-router-dom'; import { useLocation } from 'react-router'; import { Menu, Dropdown, MenuItemProps } from 'antd'; @@ -18,6 +18,7 @@ import { UISettingsActionTypes } from '../../store/actions/ui-settings'; import { RootState } from '../../store/reducers'; import { updateAboutModalVisibility } from '../../store/actions/modals'; import { triggerCopy as copyCmd } from '../../utils/copy'; +import { AdvancedModeToggle } from '../../molecules'; import useNotification from '../../hooks/useNotification'; import './Header.less'; @@ -157,6 +158,7 @@ const Header: React.FunctionComponent = ({
{token ? (
+ {name && } {name && showCreationPanel && (
{ + let history: History; + let store: Store; + let nexus; + + beforeEach(() => { + history = createBrowserHistory({ basename: '/' }); + nexus = createNexusClient({ + fetch, + uri: 'https://localhost:3000', + }); + store = configureStore(history, { nexus }, {}); + }); + afterEach(() => { + history.push('/'); + }); + it('should toggle advanced mode be in the document', async () => { + await act(async () => { + await render( + + + + + + ); + }); + await waitFor(async () => { + const toggleSwitch = await screen.getByTestId('advanced-mode-toggle'); + expect(toggleSwitch).toBeInTheDocument(); + }); + }); + it('should be checked on /data-explorer pages', () => { + history.push('/data-explorer'); + render( + + + + + + ); + + const toggleSwitch = screen.queryByTestId('advanced-mode-toggle'); + const ariaChecked = toggleSwitch?.getAttribute('aria-checked'); + expect(ariaChecked).toEqual('true'); + }); + it('should the path /data-explorer be the current path the toggle turned on', async () => { + await act(async () => { + await render( + + + + + + ); + }); + let toggleSwitch; + await waitFor(async () => { + toggleSwitch = await screen.getByTestId('advanced-mode-toggle'); + fireEvent.click(toggleSwitch); + const ariaChecked = toggleSwitch.getAttribute('aria-checked'); + console.log('@@ariaChecked', ariaChecked); + expect(ariaChecked).toEqual('true'); + }); + const currentPath = history.location.pathname; + expect(currentPath).toBe('/data-explorer'); + }); + it('should not render the toggle on blacklisted pages', () => { + history.push('/studios'); + render( + + + + + + ); + + const toggleSwitch = screen.queryByTestId('advanced-mode-toggle'); + expect(toggleSwitch).toBeNull(); + }); +}); diff --git a/src/shared/molecules/AdvancedMode/AdvancedMode.tsx b/src/shared/molecules/AdvancedMode/AdvancedMode.tsx new file mode 100644 index 000000000..0225f27a8 --- /dev/null +++ b/src/shared/molecules/AdvancedMode/AdvancedMode.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { useLocation, useHistory } from 'react-router'; +import { match as pmatch } from 'ts-pattern'; +import { Switch } from 'antd'; +import './styles.less'; + +export const advancedModeBlackList = ['/studios', '/studio']; +type TAMLocationState = { + source: string; + search: string; +}; + +const AdvancedModeToggle = () => { + const history = useHistory(); + const location = useLocation(); + const showToggle = !advancedModeBlackList.includes(location.pathname); + + const onToggle = (checked: boolean) => { + if (checked) { + history.push('/data-explorer', { + source: location.pathname, + search: location.search, + }); + } else { + history.push( + location.state.source + ? `${location.state.source}${location.state.search}` + : '/' + ); + } + }; + return pmatch(showToggle) + .with(true, () => { + return ( +
+ + Advanced Mode + Beta +
+ ); + }) + .otherwise(() => <>); +}; + +export default AdvancedModeToggle; diff --git a/src/shared/molecules/AdvancedMode/styles.less b/src/shared/molecules/AdvancedMode/styles.less new file mode 100644 index 000000000..470c8db1f --- /dev/null +++ b/src/shared/molecules/AdvancedMode/styles.less @@ -0,0 +1,47 @@ +@import '../../lib.less'; + +.advanced-mode-toggle { + display: flex; + align-items: center; + justify-content: center; + color: white; + margin-right: 15px; + .ant-switch { + margin-right: 10px; + &[aria-checked='true'] { + background-color: white !important; + .ant-switch-handle::before { + background-color: @fusion-menu-color !important; + } + } + &[aria-checked='false'] { + background-color: transparent !important; + border: 1px solid white; + .ant-switch-handle { + top: 1px !important; + } + .ant-switch-handle::before { + background-color: white !important; + } + } + } + + .advanced, + .beta { + user-select: none; + line-height: 130%; + letter-spacing: 0.01em; + } + + .advanced { + color: white; + white-space: nowrap; + margin-right: 4px; + font-weight: 400; + } + + .beta { + color: @fusion-global-red-1; + font-weight: 600; + } +} diff --git a/src/shared/molecules/index.ts b/src/shared/molecules/index.ts index 8a021ca68..8a8c397e6 100644 --- a/src/shared/molecules/index.ts +++ b/src/shared/molecules/index.ts @@ -2,6 +2,7 @@ export { default as PresetCardItem } from './PresetCardItem/PresetCardItem'; export { default as SubAppCardItem } from './SubAppCardItem/SubAppCardItem'; export { default as MyDataTable } from './MyDataTable/MyDataTable'; export { default as MyDataHeader } from './MyDataHeader/MyDataHeader'; +export { default as AdvancedModeToggle } from './AdvancedMode/AdvancedMode'; export { PresetCardItemCompact } from './PresetCardItem/PresetCardItem'; export { PresetCardItemSkeleton } from './PresetCardItem/PresetCardItem'; diff --git a/src/shared/store/actions/ui-settings.ts b/src/shared/store/actions/ui-settings.ts index 9e8ca8d46..575734293 100644 --- a/src/shared/store/actions/ui-settings.ts +++ b/src/shared/store/actions/ui-settings.ts @@ -6,6 +6,7 @@ export enum UISettingsActionTypes { CHANGE_HEADER_CREATION_PANEL = 'CHANGE_HEADER_CREATION_PANEL', UPDATE_CURRENT_RESOURCE_VIEW = 'UPDATE_CURRENT_RESOURCE_VIEW', UPDATE_JSON_EDITOR_POPOVER = 'UPDATE_JSON_EDITOR_POPOVER', + ENABLE_ADVANCED_MODE = 'ENABLE_ADVANCED_MODE', } type ChangePageSizeAction = FilterPayloadAction< diff --git a/src/shared/store/reducers/__tests__/ui-settings.ts b/src/shared/store/reducers/__tests__/ui-settings.ts index c6ebb3202..a560662c4 100644 --- a/src/shared/store/reducers/__tests__/ui-settings.ts +++ b/src/shared/store/reducers/__tests__/ui-settings.ts @@ -32,6 +32,7 @@ describe('UISettings Reducer', () => { results: [], top: 0, }, + isAdvancedModeEnabled: false, }); }); }); diff --git a/src/shared/store/reducers/ui-settings.ts b/src/shared/store/reducers/ui-settings.ts index 963a5d3cb..407444d55 100644 --- a/src/shared/store/reducers/ui-settings.ts +++ b/src/shared/store/reducers/ui-settings.ts @@ -24,6 +24,7 @@ export const DEFAULT_UI_SETTINGS: UISettingsState = { }, currentResourceView: null, editorPopoverResolvedData: editorPopoverResolvedDataInitialValue, + isAdvancedModeEnabled: false, }; export type TEditorPopoverResolvedAs = | 'resource' @@ -44,6 +45,7 @@ export interface UISettingsState { pageSizes: { [key: string]: number }; currentResourceView: Resource | null; editorPopoverResolvedData: TEditorPopoverResolvedData; + isAdvancedModeEnabled: boolean; } export default function uiSettingsReducer( @@ -85,6 +87,12 @@ export default function uiSettingsReducer( }, }; } + case UISettingsActionTypes.ENABLE_ADVANCED_MODE: { + return { + ...state, + isAdvancedModeEnabled: action.payload ?? !state.isAdvancedModeEnabled, + }; + } default: return state; } From bebaf467155752ee6b6f35d3cd59ce1a52e4b10d Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Wed, 21 Jun 2023 13:38:05 +0200 Subject: [PATCH 067/192] 1 // Show data explorer table with rows Signed-off-by: Dinika Saxena --- .../handlers/DataExplorer/handlers.ts | 57 ++++ .../DataExplorerPage/DataExplorerPage.tsx | 16 ++ .../containers/DataTableContainer.spec.tsx | 2 +- src/shared/containers/DataTableContainer.tsx | 7 +- src/shared/hooks/useAccessDataForTable.tsx | 2 +- src/shared/routes.ts | 7 + src/shared/styles/data-table.less | 10 +- .../dataExplorer/DataExplorer.spec.tsx | 263 ++++++++++++++++++ src/subapps/dataExplorer/DataExplorer.tsx | 73 +++++ .../dataExplorer/DataExplorerTable.tsx | 158 +++++++++++ src/subapps/dataExplorer/NoDataCell.tsx | 18 ++ src/subapps/dataExplorer/styles.less | 32 +++ 12 files changed, 635 insertions(+), 10 deletions(-) create mode 100644 src/__mocks__/handlers/DataExplorer/handlers.ts create mode 100644 src/pages/DataExplorerPage/DataExplorerPage.tsx create mode 100644 src/subapps/dataExplorer/DataExplorer.spec.tsx create mode 100644 src/subapps/dataExplorer/DataExplorer.tsx create mode 100644 src/subapps/dataExplorer/DataExplorerTable.tsx create mode 100644 src/subapps/dataExplorer/NoDataCell.tsx create mode 100644 src/subapps/dataExplorer/styles.less diff --git a/src/__mocks__/handlers/DataExplorer/handlers.ts b/src/__mocks__/handlers/DataExplorer/handlers.ts new file mode 100644 index 000000000..691373039 --- /dev/null +++ b/src/__mocks__/handlers/DataExplorer/handlers.ts @@ -0,0 +1,57 @@ +import { rest } from 'msw'; +import { deltaPath } from '__mocks__/handlers/handlers'; +import { Resource } from '@bbp/nexus-sdk'; + +export const dataExplorerPageHandler = (mockReources: Resource[]) => + rest.get(deltaPath(`/resources`), (req, res, ctx) => { + const mockResponse = { + '@context': [ + 'https://bluebrain.github.io/nexus/contexts/metadata.json', + 'https://bluebrain.github.io/nexus/contexts/search.json', + 'https://bluebrain.github.io/nexus/contexts/search-metadata.json', + ], + _total: 300, + _results: mockReources, + _next: + 'https://bbp.epfl.ch/nexus/v1/resources?size=50&sort=@id&after=%5B1687269183553,%22https://bbp.epfl.ch/neurosciencegraph/data/31e22529-2c36-44f0-9158-193eb50526cd%22%5D', + }; + return res(ctx.status(200), ctx.json(mockResponse)); + }); + +export const getMockResource = ( + selfSuffix: string, + extra: { [key: string]: any } +): Resource => ({ + '@id': `https://bbp.epfl.ch/neurosciencegraph/data/${selfSuffix}`, + '@type': + 'https://bbp.epfl.ch/ontologies/core/bmo/SimulationCampaignConfiguration', + _constrainedBy: + 'https://bluebrain.github.io/nexus/schemas/unconstrained.json', + _createdAt: '2023-06-21T09:39:47.217Z', + _createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/antonel', + _deprecated: false, + _incoming: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}/incoming`, + _outgoing: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}/outgoing`, + _project: + 'https://bbp.epfl.ch/nexus/v1/projects/bbp/mmb-point-neuron-framework-model', + _rev: 2, + _self: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}`, + _updatedAt: '2023-06-21T09:39:47.844Z', + _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/antonel', + ...extra, +}); + +export const defaultMockResult: Resource[] = [ + getMockResource('self1', {}), + getMockResource('self2', { specialProperty: 'superSpecialValue' }), + getMockResource('self3', { specialProperty: ['superSpecialValue'] }), + getMockResource('self4', { specialProperty: '' }), + getMockResource('self5', { specialProperty: [] }), + getMockResource('self6', { + specialProperty: ['superSpecialValue', 'so'], + }), + getMockResource('self7', { specialProperty: { foo: 1, bar: 2 } }), + getMockResource('self8', { specialProperty: null }), + getMockResource('self9', { specialProperty: {} }), + getMockResource('self10', {}), +]; diff --git a/src/pages/DataExplorerPage/DataExplorerPage.tsx b/src/pages/DataExplorerPage/DataExplorerPage.tsx new file mode 100644 index 000000000..32a2a5a4e --- /dev/null +++ b/src/pages/DataExplorerPage/DataExplorerPage.tsx @@ -0,0 +1,16 @@ +import { DataExplorer } from '../../subapps/dataExplorer/DataExplorer'; + +const DataExplorerPage = () => { + return ( +
+ +
+ ); +}; + +export default DataExplorerPage; diff --git a/src/shared/containers/DataTableContainer.spec.tsx b/src/shared/containers/DataTableContainer.spec.tsx index c071380ea..2bb11dff5 100644 --- a/src/shared/containers/DataTableContainer.spec.tsx +++ b/src/shared/containers/DataTableContainer.spec.tsx @@ -28,7 +28,7 @@ import { deltaPath } from '__mocks__/handlers/handlers'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { QueryClient, QueryClientProvider } from 'react-query'; -import configureStore from '../../shared/store'; +import configureStore from '../store'; import { cleanup, render, screen, waitFor } from '../../utils/testUtil'; import DataTableContainer from './DataTableContainer'; import { notification } from 'antd'; diff --git a/src/shared/containers/DataTableContainer.tsx b/src/shared/containers/DataTableContainer.tsx index 61788d6a9..779d139ac 100644 --- a/src/shared/containers/DataTableContainer.tsx +++ b/src/shared/containers/DataTableContainer.tsx @@ -23,21 +23,20 @@ import { } from '@ant-design/icons'; import '../styles/data-table.less'; - import { useAccessDataForTable } from '../hooks/useAccessDataForTable'; import EditTableForm, { Projection } from '../components/EditTableForm'; import { useMutation } from 'react-query'; import { parseProjectUrl } from '../utils'; import useNotification from '../hooks/useNotification'; -import { ErrorComponent } from '../../shared/components/ErrorComponent'; +import { ErrorComponent } from '../components/ErrorComponent'; import { useSelector } from 'react-redux'; import { RootState } from 'shared/store/reducers'; import { DATA_PANEL_STORAGE, DATA_PANEL_STORAGE_EVENT, DataPanelEvent, -} from '../../shared/organisms/DataPanel/DataPanel'; -import { TResourceTableData } from '../../shared/molecules/MyDataTable/MyDataTable'; +} from '../organisms/DataPanel/DataPanel'; +import { TResourceTableData } from '../molecules/MyDataTable/MyDataTable'; export type TableColumn = { '@type': string; diff --git a/src/shared/hooks/useAccessDataForTable.tsx b/src/shared/hooks/useAccessDataForTable.tsx index d30de3019..abae5ac05 100644 --- a/src/shared/hooks/useAccessDataForTable.tsx +++ b/src/shared/hooks/useAccessDataForTable.tsx @@ -22,7 +22,7 @@ import { TableError, getStudioLocalStorageKey, getStudioTableKey, -} from '../../shared/containers/DataTableContainer'; +} from '../containers/DataTableContainer'; import { MAX_DATA_SELECTED_SIZE__IN_BYTES, MAX_LOCAL_STORAGE_ALLOWED_SIZE, diff --git a/src/shared/routes.ts b/src/shared/routes.ts index 084bfc6a0..7ce53035c 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -7,6 +7,7 @@ import IdentityPage from '../pages/IdentityPage/IdentityPage'; import StudioRedirectView from './views/StudioRedirectView'; import MyDataPage from '../pages/MyDataPage/MyDataPage'; import DataExplorerResolverPage from '../pages/DataExplorerGraphFlowPage/DataExplorerGraphFlowPage'; +import DataExplorerPage from '../pages/DataExplorerPage/DataExplorerPage'; type TRoutePropsExtended = RouteProps & { protected: boolean }; @@ -46,6 +47,12 @@ const routes: TRoutePropsExtended[] = [ exact: true, protected: true, }, + { + path: '/data-explorer', + component: DataExplorerPage, + exact: true, + protected: true, + }, { path: '/:orgLabel/:projectLabel/resources/:resourceId', component: ResourceView, diff --git a/src/shared/styles/data-table.less b/src/shared/styles/data-table.less index 7a37b738e..6ad52939b 100644 --- a/src/shared/styles/data-table.less +++ b/src/shared/styles/data-table.less @@ -27,10 +27,12 @@ h3.ant-typography.table-title { margin: 0 20px; } -.ant-pagination-options .ant-pagination-options-size-changer { - display: none !important; -} - .studio-table-container { margin-bottom: 60px; // Leave space for the data-panel that shows cart objects } + +.studio-table-container + .ant-pagination-options + .ant-pagination-options-size-changer { + display: none !important; +} diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx new file mode 100644 index 000000000..6e12a7d8f --- /dev/null +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -0,0 +1,263 @@ +import { Resource, createNexusClient } from '@bbp/nexus-sdk'; +import { NexusProvider } from '@bbp/react-nexus'; +import '@testing-library/jest-dom'; +import { RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +import { + dataExplorerPageHandler, + defaultMockResult, + getMockResource, +} from '__mocks__/handlers/DataExplorer/handlers'; +import { deltaPath } from '__mocks__/handlers/handlers'; +import { setupServer } from 'msw/node'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { render, screen, waitFor } from '../../utils/testUtil'; +import { DataExplorer } from './DataExplorer'; + +describe('DataExplorer', () => { + const server = setupServer(dataExplorerPageHandler(defaultMockResult)); + let container: HTMLElement; + let user: UserEvent; + let component: RenderResult; + let dataExplorerPage: JSX.Element; + + beforeEach(async () => { + server.listen(); + const queryClient = new QueryClient(); + const nexus = createNexusClient({ + fetch, + uri: deltaPath(), + }); + + dataExplorerPage = ( + + + + + + ); + + component = render(dataExplorerPage); + + container = component.container; + user = userEvent.setup(); + }); + + afterEach(() => { + server.resetHandlers(); + }); + afterAll(() => { + server.resetHandlers(); + server.close(); + }); + + const expectRowCountToBe = async (expectedRowsCount: number) => { + return await waitFor(() => { + const rows = visibleTableRows(); + expect(rows.length).toEqual(expectedRowsCount); + return rows; + }); + }; + + const expectColumHeaderToExist = async (name: string) => { + const nameReg = new RegExp(`^${name}`, 'i'); + const header = await screen.getByText(nameReg, { + selector: 'th', + exact: false, + }); + expect(header).toBeInTheDocument(); + return header; + }; + + const expectedHeader = (propName: string) => { + switch (propName) { + case '_project': + return 'PROJECT'; + case '@type': + return 'TYPE'; + default: + return propName; + } + }; + + const getTextForColumn = async (resource: Resource, colName: string) => { + const selfCell = await screen.getAllByText( + new RegExp(resource._self, 'i'), + { + selector: 'td', + } + ); + + const allCellsForRow = Array.from(selfCell[0].parentElement!.childNodes); + const colIndex = Array.from( + container.querySelectorAll('th') + ).findIndex(header => header.innerHTML.match(new RegExp(colName, 'i'))); + return allCellsForRow[colIndex].textContent; + }; + + const visibleTableRows = () => { + return container.querySelectorAll('table tbody tr.data-explorer-row'); + }; + + it('shows rows for all fetched resources', async () => { + await expectRowCountToBe(10); + }); + + it('shows columns for each top level property in resources', async () => { + await expectRowCountToBe(10); + const seenColumns = new Set(); + + for (const mockResource of defaultMockResult) { + for (const topLevelProperty of Object.keys(mockResource)) { + if (!seenColumns.has(topLevelProperty)) { + seenColumns.add(topLevelProperty); + const header = expectedHeader(topLevelProperty); + await expectColumHeaderToExist(header); + } + } + } + + expect(seenColumns.size).toBeGreaterThan(1); + }); + + it('shows project as the first column', async () => { + await expectRowCountToBe(10); + const firstColumn = container.querySelector('th.data-explorer-column'); + expect(firstColumn?.textContent).toMatch(/project/i); + }); + + it('shows type as the second column', async () => { + await expectRowCountToBe(10); + const secondColumn = container.querySelectorAll( + 'th.data-explorer-column' + )[1]; + expect(secondColumn?.textContent).toMatch(/type/i); + }); + + it('updates columns when new page is selected', async () => { + await expectRowCountToBe(10); + + server.use( + dataExplorerPageHandler([ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]) + ); + + const pageInput = await screen.getByRole('listitem', { name: '2' }); + expect(pageInput).toBeInTheDocument(); + + await user.click(pageInput); + + await expectRowCountToBe(3); + + await expectColumHeaderToExist('author'); + await expectColumHeaderToExist('edition'); + await expectColumHeaderToExist('year'); + }); + + it('updates page size', async () => { + await expectRowCountToBe(10); + + const mock100Resources: Resource[] = []; + + for (let i = 0; i < 100; i = i + 1) { + mock100Resources.push(getMockResource(`self${i}`, {})); + } + + server.use(dataExplorerPageHandler(mock100Resources)); + + const pageSizeChanger = await screen.getByRole('combobox', { + name: 'Page Size', + }); + await userEvent.click(pageSizeChanger); + const twentyRowsOption = await screen.getByTitle('20 / page'); + await userEvent.click(twentyRowsOption, { pointerEventsCheck: 0 }); + await expectRowCountToBe(20); + }); + + it('shows No data text when values are missing for a column', async () => { + await expectRowCountToBe(10); + const resourceWithMissingProperty = defaultMockResult.find( + res => !('specialProperty' in res) + )!; + const textForSpecialProperty = await getTextForColumn( + resourceWithMissingProperty, + 'specialProperty' + ); + expect(textForSpecialProperty).toMatch(/No data/i); + }); + + it('shows No data text when values is undefined', async () => { + await expectRowCountToBe(10); + const resourceWithUndefinedProperty = defaultMockResult.find( + res => res.specialProperty === undefined + )!; + const textForSpecialProperty = await getTextForColumn( + resourceWithUndefinedProperty, + 'specialProperty' + ); + expect(textForSpecialProperty).toMatch(/No data/i); + }); + + it('shows No data text when values is null', async () => { + await expectRowCountToBe(10); + const resourceWithUndefinedProperty = defaultMockResult.find( + res => res.specialProperty === null + )!; + const textForSpecialProperty = await getTextForColumn( + resourceWithUndefinedProperty, + 'specialProperty' + ); + expect(textForSpecialProperty).toMatch(/No data/i); + }); + + it('does not show No data when value is empty string', async () => { + await expectRowCountToBe(10); + const resourceWithEmptyString = defaultMockResult.find( + res => res.specialProperty === '' + )!; + + const textForSpecialProperty = await getTextForColumn( + resourceWithEmptyString, + 'specialProperty' + ); + expect(textForSpecialProperty).not.toMatch(/No data/i); + expect(textForSpecialProperty).toEqual('""'); + }); + + it('does not show No data when value is empty array', async () => { + await expectRowCountToBe(10); + const resourceWithEmptyArray = defaultMockResult.find( + res => + Array.isArray(res.specialProperty) && res.specialProperty.length === 0 + )!; + + const textForSpecialProperty = await getTextForColumn( + resourceWithEmptyArray, + 'specialProperty' + ); + expect(textForSpecialProperty).not.toMatch(/No data/i); + expect(textForSpecialProperty).toEqual('[]'); + }); + + it('does not show No data when value is empty object', async () => { + await expectRowCountToBe(10); + const resourceWithEmptyObject = defaultMockResult.find( + res => + typeof res.specialProperty === 'object' && + res.specialProperty !== null && + !Array.isArray(res.specialProperty) && + Object.keys(res.specialProperty).length === 0 + )!; + + const textForSpecialProperty = await getTextForColumn( + resourceWithEmptyObject, + 'specialProperty' + ); + expect(textForSpecialProperty).not.toMatch(/No data/i); + expect(textForSpecialProperty).toEqual('{}'); + }); +}); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx new file mode 100644 index 000000000..1e93bf2ed --- /dev/null +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -0,0 +1,73 @@ +import { Resource } from '@bbp/nexus-sdk'; +import { useNexusContext } from '@bbp/react-nexus'; +import { notification } from 'antd'; +import { isObject, isString } from 'lodash'; +import React, { useReducer } from 'react'; +import { useQuery } from 'react-query'; +import { getResourceLabel } from '../../shared/utils'; +import { DataExplorerTable } from './DataExplorerTable'; +import './styles.less'; + +export interface DataExplorerConfiguration { + pageSize: number; + offset: number; +} + +export const DataExplorer: React.FC<{}> = () => { + const nexus = useNexusContext(); + + const [{ pageSize, offset }, updateTableConfiguration] = useReducer( + ( + previous: DataExplorerConfiguration, + next: Partial + ) => ({ ...previous, ...next }), + { pageSize: 50, offset: 0 } + ); + + const { data: resources, isLoading } = useQuery({ + queryKey: ['data-explorer', { pageSize, offset }], + retry: false, + queryFn: () => { + return nexus.Resource.list(undefined, undefined, { + from: offset, + size: pageSize, + }); + }, + onError: error => { + notification.error({ + message: 'Error loading data from the server', + description: isString(error) ? ( + error + ) : isObject(error) ? ( +
+ {(error as any)['@type']} +
{(error as any)['details']}
+
+ ) : ( + '' + ), + }); + }, + }); + + const dataSource: Resource[] = + resources?._results?.map(resource => { + return { + ...resource, + name: getResourceLabel(resource), + }; + }) || []; + + return ( +
+ +
+ ); +}; diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx new file mode 100644 index 000000000..0e63f61a5 --- /dev/null +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -0,0 +1,158 @@ +import { Resource } from '@bbp/nexus-sdk'; +import { Empty, Table, Tooltip } from 'antd'; +import { ColumnType, TablePaginationConfig } from 'antd/lib/table'; +import { isArray, isString } from 'lodash'; +import React from 'react'; +import { makeOrgProjectTuple } from '../../shared/molecules/MyDataTable/MyDataTable'; +import isValidUrl from '../../utils/validUrl'; +import { NoDataCell } from './NoDataCell'; +import './styles.less'; +import { DataExplorerConfiguration } from './DataExplorer'; + +interface TDataExplorerTable { + isLoading: boolean; + dataSource: Resource[]; + total?: number; + pageSize: number; + offset: number; + updateTableConfiguration: React.Dispatch>; +} + +type TColumnNameToConfig = Map>; + +export const DataExplorerTable: React.FC = ({ + isLoading, + dataSource, + total, + pageSize, + offset, + updateTableConfiguration, +}: TDataExplorerTable) => { + const allowedTotal = total ? (total > 10000 ? 10000 : total) : undefined; + + const tablePaginationConfig: TablePaginationConfig = { + pageSize, + total: allowedTotal, + 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, + }; + + return ( + <> + + sticky={{ + offsetHeader: 50, + getContainer: () => window, + }} + columns={dynamicColumnsForDataSource(dataSource)} + dataSource={dataSource} + rowKey={record => record._self} + loading={isLoading} + bordered={false} + className="data-explorer-table" + rowClassName="data-explorer-row" + scroll={{ x: 'max-content', y: '100vh' }} + locale={{ + emptyText() { + return isLoading ? <> : ; + }, + }} + pagination={tablePaginationConfig} + /> + + ); +}; + +/** + * 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). + */ +const dynamicColumnsForDataSource = ( + resources: Resource[] +): ColumnType[] => { + const colNameToConfig = new Map(initialTableConfig()); + + resources.forEach(resource => { + const newResourceKeys = Object.keys(resource).filter( + key => !colNameToConfig.has(key) + ); + + newResourceKeys.forEach(key => { + colNameToConfig.set(key, { ...defaultColumnConfig(key) }); + }); + }); + + return Array.from(colNameToConfig.values()); +}; + +const defaultColumnConfig = (colName: string): ColumnType => { + return { + key: colName, + title: colName.toUpperCase(), + dataIndex: colName, + className: `data-explorer-column data-explorer-column-${colName}`, + sorter: false, + render: text => { + if (text === undefined || text === null) { + return ; + } + return <>{JSON.stringify(text)}; + }, + }; +}; + +const initialTableConfig = () => { + const colNameToConfig: TColumnNameToConfig = new Map(); + const projectKey = '_project'; + const typeKey = '@type'; + + const projectConfig: ColumnType = { + ...defaultColumnConfig(projectKey), + title: 'PROJECT', + render: text => { + if (text) { + const { org, project } = makeOrgProjectTuple(text); + return `${org}/${project}`; + } + return ; + }, + }; + const typeConfig: ColumnType = { + ...defaultColumnConfig(typeKey), + title: 'TYPE', + render: text => { + let types = ''; + if (isArray(text)) { + types = text + .map(item => (isValidUrl(item) ? item.split('/').pop() : item)) + .join('\n'); + } else if (isString(text) && isValidUrl(text)) { + types = text.split('/').pop() ?? ''; + } else { + types = text; + } + return types ? ( +
{text}
} + > +
{types}
+
+ ) : ( + + ); + }, + }; + + colNameToConfig.set(projectKey, projectConfig); + colNameToConfig.set(typeKey, typeConfig); + + return colNameToConfig; +}; diff --git a/src/subapps/dataExplorer/NoDataCell.tsx b/src/subapps/dataExplorer/NoDataCell.tsx new file mode 100644 index 000000000..bff648feb --- /dev/null +++ b/src/subapps/dataExplorer/NoDataCell.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +export const NoDataCell: React.FC<{}> = () => { + return ( +
+ No data +
+ ); +}; diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less new file mode 100644 index 000000000..27733da6e --- /dev/null +++ b/src/subapps/dataExplorer/styles.less @@ -0,0 +1,32 @@ +@import '../../shared/lib.less'; + +.container { + margin-bottom: 52px; + padding-top: 72px; + padding-left: 40px; + background: @fusion-main-bg; +} + +.ant-table-thead > tr > th.data-explorer-column { + background-color: #f5f5f5 !important; + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 130%; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #8c8c8c; + height: 52px; + min-width: 72px; +} + +.ant-table-body tr td { + background-color: @fusion-main-bg; + min-width: 72px; + color: @fusion-blue-8; +} + +.ant-table-tbody > tr.data-explorer-row > td { + border-bottom: 1px solid #d9d9d9; +} From 70b794c2a3927972f3a596a8a3b83f302a8e3988 Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Thu, 22 Jun 2023 16:38:00 +0200 Subject: [PATCH 068/192] US 2 // Allow users to search resources by project Signed-off-by: Dinika Saxena --- .../handlers/DataExplorer/handlers.ts | 95 +++++++++++++++++-- .../DataExplorerPage/DataExplorerPage.tsx | 7 +- src/shared/App.less | 6 ++ .../dataExplorer/DataExplorer.spec.tsx | 79 ++++++++++++++- src/subapps/dataExplorer/DataExplorer.tsx | 28 +++++- .../dataExplorer/DataExplorerTable.tsx | 11 +-- src/subapps/dataExplorer/NoDataCell.tsx | 3 +- src/subapps/dataExplorer/ProjectSelector.tsx | 86 +++++++++++++++++ src/subapps/dataExplorer/styles.less | 95 +++++++++++++++---- 9 files changed, 358 insertions(+), 52 deletions(-) create mode 100644 src/subapps/dataExplorer/ProjectSelector.tsx diff --git a/src/__mocks__/handlers/DataExplorer/handlers.ts b/src/__mocks__/handlers/DataExplorer/handlers.ts index 691373039..5f0b0076d 100644 --- a/src/__mocks__/handlers/DataExplorer/handlers.ts +++ b/src/__mocks__/handlers/DataExplorer/handlers.ts @@ -1,6 +1,6 @@ import { rest } from 'msw'; import { deltaPath } from '__mocks__/handlers/handlers'; -import { Resource } from '@bbp/nexus-sdk'; +import { Project, Resource } from '@bbp/nexus-sdk'; export const dataExplorerPageHandler = (mockReources: Resource[]) => rest.get(deltaPath(`/resources`), (req, res, ctx) => { @@ -18,9 +18,87 @@ export const dataExplorerPageHandler = (mockReources: Resource[]) => return res(ctx.status(200), ctx.json(mockResponse)); }); +export const filterByProjectHandler = (mockReources: Resource[]) => + rest.get(deltaPath(`/resources/:org/:project`), (req, res, ctx) => { + const { project } = req.params; + + const responseBody = project + ? mockReources.filter( + res => + res._project.slice(res._project.lastIndexOf('/') + 1) === project + ) + : mockReources; + const mockResponse = { + '@context': [ + 'https://bluebrain.github.io/nexus/contexts/metadata.json', + 'https://bluebrain.github.io/nexus/contexts/search.json', + 'https://bluebrain.github.io/nexus/contexts/search-metadata.json', + ], + _total: 300, + _results: responseBody, + _next: + 'https://bbp.epfl.ch/nexus/v1/resources?size=50&sort=@id&after=%5B1687269183553,%22https://bbp.epfl.ch/neurosciencegraph/data/31e22529-2c36-44f0-9158-193eb50526cd%22%5D', + }; + return res(ctx.status(200), ctx.json(mockResponse)); + }); + +export const getProjectHandler = () => + rest.get(deltaPath(`/projects`), (req, res, ctx) => { + const projectResponse = { + '@context': [ + 'https://bluebrain.github.io/nexus/contexts/metadata.json', + 'https://bluebrain.github.io/nexus/contexts/search.json', + 'https://bluebrain.github.io/nexus/contexts/projects.json', + ], + _next: + 'https://bbp.epfl.ch/nexus/v1/projects?from=10&label=&size=10&sort=_label', + _total: 10, + _results: [ + getMockProject('something-brainy', 'bbp'), + + getMockProject('smarty', 'bbp'), + + getMockProject('unhcr', 'un'), + getMockProject('unicef', 'un'), + getMockProject('tellytubbies', 'bbc'), + ], + }; + return res(ctx.status(200), ctx.json(projectResponse)); + }); + +const getMockProject = (project: string, org: string = 'bbp'): Project => { + return { + '@id': `https://bbp.epfl.ch/nexus/v1/projects/bbp/${project}`, + '@type': 'Project', + apiMappings: [ + { + namespace: 'https://neuroshapes.org/dash/', + prefix: 'datashapes', + }, + ], + '@context': ['mockcontext'], + base: 'https://bbp.epfl.ch/neurosciencegraph/data/', + description: 'This is such a dumb mock project. dumb dumb dumb.', + vocab: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/Blue-Brain-Ketogenic-Project-(BBK)/_/', + _constrainedBy: 'https://bluebrain.github.io/nexus/schemas/projects.json', + _createdAt: '2021-03-04T21:27:18.900Z', + _createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/harikris', + _deprecated: true, + _label: `${project}`, + _organizationLabel: org, + _organizationUuid: 'a605b71a-377d-4df3-95f8-923149d04106', + _rev: 2, + _self: `https://bbp.epfl.ch/nexus/v1/projects/bbp/${project}`, + _updatedAt: '2021-03-15T09:05:05.882Z', + _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/harikris', + }; +}; + export const getMockResource = ( selfSuffix: string, - extra: { [key: string]: any } + extra: { [key: string]: any }, + project: string = 'hippocampus' ): Resource => ({ '@id': `https://bbp.epfl.ch/neurosciencegraph/data/${selfSuffix}`, '@type': @@ -30,12 +108,11 @@ export const getMockResource = ( _createdAt: '2023-06-21T09:39:47.217Z', _createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/antonel', _deprecated: false, - _incoming: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}/incoming`, - _outgoing: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}/outgoing`, - _project: - 'https://bbp.epfl.ch/nexus/v1/projects/bbp/mmb-point-neuron-framework-model', + _incoming: `https://bbp.epfl.ch/nexus/v1/resources/bbp/${project}/_/${selfSuffix}/incoming`, + _outgoing: `https://bbp.epfl.ch/nexus/v1/resources/bbp/${project}/_/${selfSuffix}/outgoing`, + _project: `https://bbp.epfl.ch/nexus/v1/projects/bbp/${project}`, _rev: 2, - _self: `https://bbp.epfl.ch/nexus/v1/resources/bbp/mmb-point-neuron-framework-model/_/${selfSuffix}`, + _self: `https://bbp.epfl.ch/nexus/v1/resources/bbp/${project}/_/${selfSuffix}`, _updatedAt: '2023-06-21T09:39:47.844Z', _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/antonel', ...extra, @@ -43,14 +120,14 @@ export const getMockResource = ( export const defaultMockResult: Resource[] = [ getMockResource('self1', {}), - getMockResource('self2', { specialProperty: 'superSpecialValue' }), + getMockResource('self2', { specialProperty: 'superSpecialValue' }, 'unhcr'), getMockResource('self3', { specialProperty: ['superSpecialValue'] }), getMockResource('self4', { specialProperty: '' }), getMockResource('self5', { specialProperty: [] }), getMockResource('self6', { specialProperty: ['superSpecialValue', 'so'], }), - getMockResource('self7', { specialProperty: { foo: 1, bar: 2 } }), + getMockResource('self7', { specialProperty: { foo: 1, bar: 2 } }, 'unhcr'), getMockResource('self8', { specialProperty: null }), getMockResource('self9', { specialProperty: {} }), getMockResource('self10', {}), diff --git a/src/pages/DataExplorerPage/DataExplorerPage.tsx b/src/pages/DataExplorerPage/DataExplorerPage.tsx index 32a2a5a4e..8d4993153 100644 --- a/src/pages/DataExplorerPage/DataExplorerPage.tsx +++ b/src/pages/DataExplorerPage/DataExplorerPage.tsx @@ -2,12 +2,7 @@ import { DataExplorer } from '../../subapps/dataExplorer/DataExplorer'; const DataExplorerPage = () => { return ( -
+
); diff --git a/src/shared/App.less b/src/shared/App.less index 98a35ed6e..adf84b82a 100644 --- a/src/shared/App.less +++ b/src/shared/App.less @@ -30,6 +30,12 @@ background-color: @background-color-subtle; } } + &.data-explorer-container { + width: fit-content; + padding-right: 0; + padding-top: 0; + margin-right: 1rem; + } } .graph-wrapper-container { diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 6e12a7d8f..f16c5e821 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -7,16 +7,25 @@ import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import { dataExplorerPageHandler, defaultMockResult, + filterByProjectHandler, getMockResource, + getProjectHandler, } from '__mocks__/handlers/DataExplorer/handlers'; import { deltaPath } from '__mocks__/handlers/handlers'; import { setupServer } from 'msw/node'; import { QueryClient, QueryClientProvider } from 'react-query'; import { render, screen, waitFor } from '../../utils/testUtil'; import { DataExplorer } from './DataExplorer'; +import { AllProjects } from './ProjectSelector'; +import { getColumnTitle } from './DataExplorerTable'; describe('DataExplorer', () => { - const server = setupServer(dataExplorerPageHandler(defaultMockResult)); + const server = setupServer( + dataExplorerPageHandler(defaultMockResult), + filterByProjectHandler(defaultMockResult), + getProjectHandler() + ); + let container: HTMLElement; let user: UserEvent; let component: RenderResult; @@ -61,7 +70,7 @@ describe('DataExplorer', () => { }; const expectColumHeaderToExist = async (name: string) => { - const nameReg = new RegExp(`^${name}`, 'i'); + const nameReg = new RegExp(getColumnTitle(name), 'i'); const header = await screen.getByText(nameReg, { selector: 'th', exact: false, @@ -92,14 +101,54 @@ describe('DataExplorer', () => { const allCellsForRow = Array.from(selfCell[0].parentElement!.childNodes); const colIndex = Array.from( container.querySelectorAll('th') - ).findIndex(header => header.innerHTML.match(new RegExp(colName, 'i'))); + ).findIndex(header => + header.innerHTML.match(new RegExp(getColumnTitle(colName), 'i')) + ); return allCellsForRow[colIndex].textContent; }; + const openProjectAutocomplete = async () => { + const projectAutocomplete = await getProjectAutocomplete(); + await userEvent.click(projectAutocomplete); + return projectAutocomplete; + }; + + const selectProject = async (projectName: string) => { + await openProjectAutocomplete(); + const unhcrProject = await getProjectOption(projectName); + await userEvent.click(unhcrProject, { pointerEventsCheck: 0 }); + }; + + const searchForProject = async (searchTerm: string) => { + const projectAutocomplete = await openProjectAutocomplete(); + await userEvent.clear(projectAutocomplete); + await userEvent.type(projectAutocomplete, searchTerm); + return projectAutocomplete; + }; + + const expectProjectOptionsToMatch = async (searchTerm: string) => { + const projectOptions = await screen.getAllByRole('option'); + expect(projectOptions.length).toBeGreaterThan(0); + projectOptions.forEach(option => { + expect(option.innerHTML).toMatch(new RegExp(searchTerm, 'i')); + }); + }; + + const projectFromRow = (row: Element) => { + const projectColumn = row.querySelector('td'); // first column is the project column + return projectColumn?.textContent; + }; + const visibleTableRows = () => { return container.querySelectorAll('table tbody tr.data-explorer-row'); }; + const getProjectAutocomplete = async () => { + return await screen.getByLabelText('project-filter', { + selector: 'input', + }); + }; + it('shows rows for all fetched resources', async () => { await expectRowCountToBe(10); }); @@ -260,4 +309,28 @@ describe('DataExplorer', () => { expect(textForSpecialProperty).not.toMatch(/No data/i); expect(textForSpecialProperty).toEqual('{}'); }); + + const getProjectOption = async (projectName: string) => + await screen.getByText(new RegExp(projectName, 'i'), { + selector: 'div.ant-select-item-option-content', + }); + + it('shows resources filtered by the selected project', async () => { + await selectProject('unhcr'); + + visibleTableRows().forEach(row => + expect(projectFromRow(row)).toMatch(/unhcr/i) + ); + + await selectProject(AllProjects); + await expectRowCountToBe(10); + }); + + it('shows autocomplete options for project filter', async () => { + await searchForProject('bbp'); + await expectProjectOptionsToMatch('bbp'); + + await searchForProject('bbc'); + await expectProjectOptionsToMatch('bbc'); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 1e93bf2ed..d5654adfa 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -7,28 +7,33 @@ import { useQuery } from 'react-query'; import { getResourceLabel } from '../../shared/utils'; import { DataExplorerTable } from './DataExplorerTable'; import './styles.less'; +import { ProjectSelector } from './ProjectSelector'; export interface DataExplorerConfiguration { pageSize: number; offset: number; + orgAndProject?: [string, string]; } export const DataExplorer: React.FC<{}> = () => { const nexus = useNexusContext(); - const [{ pageSize, offset }, updateTableConfiguration] = useReducer( + const [ + { pageSize, offset, orgAndProject }, + updateTableConfiguration, + ] = useReducer( ( previous: DataExplorerConfiguration, next: Partial ) => ({ ...previous, ...next }), - { pageSize: 50, offset: 0 } + { pageSize: 50, offset: 0, orgAndProject: undefined } ); const { data: resources, isLoading } = useQuery({ - queryKey: ['data-explorer', { pageSize, offset }], + queryKey: ['data-explorer', { pageSize, offset, orgAndProject }], retry: false, queryFn: () => { - return nexus.Resource.list(undefined, undefined, { + return nexus.Resource.list(orgAndProject?.[0], orgAndProject?.[1], { from: offset, size: pageSize, }); @@ -59,7 +64,20 @@ export const DataExplorer: React.FC<{}> = () => { }) || []; return ( -
+
+
+ { + if (orgLabel && projectLabel) { + updateTableConfiguration({ + orgAndProject: [orgLabel, projectLabel], + }); + } else { + updateTableConfiguration({ orgAndProject: undefined }); + } + }} + /> +
= ({ return ( <> - sticky={{ - offsetHeader: 50, - getContainer: () => window, - }} columns={dynamicColumnsForDataSource(dataSource)} dataSource={dataSource} rowKey={record => record._self} @@ -93,10 +89,13 @@ const dynamicColumnsForDataSource = ( return Array.from(colNameToConfig.values()); }; +export const getColumnTitle = (colName: string) => + startCase(colName).toUpperCase(); + const defaultColumnConfig = (colName: string): ColumnType => { return { key: colName, - title: colName.toUpperCase(), + title: getColumnTitle(colName), dataIndex: colName, className: `data-explorer-column data-explorer-column-${colName}`, sorter: false, diff --git a/src/subapps/dataExplorer/NoDataCell.tsx b/src/subapps/dataExplorer/NoDataCell.tsx index bff648feb..2146f8d14 100644 --- a/src/subapps/dataExplorer/NoDataCell.tsx +++ b/src/subapps/dataExplorer/NoDataCell.tsx @@ -8,8 +8,7 @@ export const NoDataCell: React.FC<{}> = () => { backgroundColor: '#ffd9d9', fontWeight: 600, lineHeight: '17.5px', - padding: '5px', - paddingLeft: '10px', + padding: '5px 10px', }} > No data diff --git a/src/subapps/dataExplorer/ProjectSelector.tsx b/src/subapps/dataExplorer/ProjectSelector.tsx new file mode 100644 index 000000000..c6a0fc1d2 --- /dev/null +++ b/src/subapps/dataExplorer/ProjectSelector.tsx @@ -0,0 +1,86 @@ +import { ProjectResponseCommon } from '@bbp/nexus-sdk'; +import { useNexusContext } from '@bbp/react-nexus'; +import { AutoComplete, Input, notification } from 'antd'; +import React, { useState } from 'react'; +import { useQuery } from 'react-query'; +import './styles.less'; + +interface Props { + onSelect: (orgLabel?: string, projectLabel?: string) => void; +} + +export const ProjectSelector: React.FC = ({ onSelect }: Props) => { + const { data: projects } = useProjects(); + const [searchTerm, setSearchTerm] = useState(''); + + const allOptions = [ + { value: AllProjects, label: AllProjects }, + ...(projects?.map(projectToOption) ?? []), + ].filter(project => project.value.includes(searchTerm)); + + return ( +
+ Show me + { + setSearchTerm(normalizeSearchTerm(text)); + }} + onSelect={text => { + if (text === AllProjects) { + onSelect(undefined, undefined); + } else { + const [org, project] = text.split('/'); + onSelect(org, project); + } + }} + aria-label="project-filter" + bordered={false} + className="search-input" + popupClassName="search-menu" + > + + +
+ ); +}; + +export const AllProjects = 'All Projects'; + +const projectToOption = ( + project: ProjectResponseCommon +): { value: string; label: string } => { + return { + value: `${project._organizationLabel}/${project._label}`, + label: `${project._organizationLabel}/${project._label}`, + }; +}; + +const useProjects = () => { + const nexus = useNexusContext(); + return useQuery({ + queryKey: ['data-explorer-projects'], + retry: false, + queryFn: async () => { + // TODO: Replace this with aggregation API when it is ready. + // NOTE: At the moment it is not possible to get all projects a user has access to in 1 request. To get around this, we make 2 requests: + // 1st request -> Get the total number of projects (n) a user can see + // 2nd request -> Use the total retrieved from above request to specify the size of the projects to return. + const firstPageOfProjects = await nexus.Project.list(undefined, { + size: 1, + }); + const allProjects = await nexus.Project.list(undefined, { + size: firstPageOfProjects._total, + }); + + return allProjects._results; + }, + onError: error => { + notification.error({ message: 'Projects could not be fetched' }); + }, + }); +}; + +const normalizeSearchTerm = (text: string) => text.trim().toLowerCase(); diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 27733da6e..93aaf0561 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -1,32 +1,85 @@ @import '../../shared/lib.less'; -.container { +.data-explorer-contents { margin-bottom: 52px; - padding-top: 72px; padding-left: 40px; background: @fusion-main-bg; -} -.ant-table-thead > tr > th.data-explorer-column { - background-color: #f5f5f5 !important; - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 400; - font-size: 12px; - line-height: 130%; - letter-spacing: 0.04em; - text-transform: uppercase; - color: #8c8c8c; - height: 52px; - min-width: 72px; + .search-container { + display: flex; + align-items: center; + flex-direction: row; + max-width: fit-content; + + .label { + font-size: 12px; + font-weight: 300; + line-height: 16px; + letter-spacing: 0.01em; + text-align: left; + color: @fusion-blue-8; + } + + .search-input { + border-bottom: 1px solid @fusion-neutral-7; + margin-left: 8px !important; + + input { + color: @fusion-blue-8 !important; + border: none; + background: @fusion-main-bg; + font-weight: 700; + } + + button { + background: #f5f5f5; + border: unset; + } + } + } } -.ant-table-body tr td { - background-color: @fusion-main-bg; - min-width: 72px; - color: @fusion-blue-8; +.search-menu { + .ant-select-item-option-content { + color: @fusion-blue-8; + } } -.ant-table-tbody > tr.data-explorer-row > td { - border-bottom: 1px solid #d9d9d9; +.data-explorer-table { + table { + width: auto; + min-width: unset !important; + } + + .ant-table-thead > tr > th.data-explorer-column { + background-color: #f5f5f5 !important; + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 130%; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #8c8c8c; + height: 52px; + min-width: 72px; + } + + .ant-table-thead > tr > th::before { + font-size: 0px; // Removes the little vertical divider between table header cells + } + + th.ant-table-cell.ant-table-cell-scrollbar { + background-color: #f5f5f5 !important; + } + + .ant-table-body tr td { + background-color: @fusion-main-bg; + min-width: 72px; + color: @fusion-blue-8; + } + + .ant-table-tbody > tr.data-explorer-row > td { + border-bottom: 1px solid #d9d9d9; + } } From d30219e6679cf4419db956093aa3b782c183d294 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 4 Jul 2023 17:11:01 +0200 Subject: [PATCH 069/192] de-12: rename mocks folder for graph flow --- .../{DataExplorer => DataExplorerGraphFlow}/handlers.ts | 0 .../canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/__mocks__/handlers/{DataExplorer => DataExplorerGraphFlow}/handlers.ts (100%) diff --git a/src/__mocks__/handlers/DataExplorer/handlers.ts b/src/__mocks__/handlers/DataExplorerGraphFlow/handlers.ts similarity index 100% rename from src/__mocks__/handlers/DataExplorer/handlers.ts rename to src/__mocks__/handlers/DataExplorerGraphFlow/handlers.ts diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx index 92c6d4ca2..8f48c74d5 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx @@ -20,7 +20,7 @@ import { initialResource, getDataExplorerGraphFlowResourceObject, getDataExplorerGraphFlowResourceObjectTags, -} from '../../../__mocks__/handlers/DataExplorer/handlers'; +} from '../../../__mocks__/handlers/DataExplorerGraphFlow/handlers'; const initialDataExplorerState = { current: { From ed5ac4fdd4c3dfccb92004f3962bb9bfcdccf472 Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Tue, 27 Jun 2023 13:22:12 +0200 Subject: [PATCH 070/192] US 3 // Filter resources that have path missing Signed-off-by: Dinika Saxena --- .../handlers/DataExplorer/handlers.ts | 2 +- src/shared/App.less | 5 +- src/shared/layouts/FusionMainLayout.less | 7 +- .../dataExplorer/DataExplorer-utils.spec.tsx | 160 ++++++++++++++++++ .../dataExplorer/DataExplorer.spec.tsx | 150 +++++++++++----- src/subapps/dataExplorer/DataExplorer.tsx | 51 ++++-- .../dataExplorer/DataExplorerTable.tsx | 58 +++---- .../dataExplorer/PredicateSelector.tsx | 132 +++++++++++++++ src/subapps/dataExplorer/ProjectSelector.tsx | 2 +- src/subapps/dataExplorer/styles.less | 37 +++- 10 files changed, 512 insertions(+), 92 deletions(-) create mode 100644 src/subapps/dataExplorer/DataExplorer-utils.spec.tsx create mode 100644 src/subapps/dataExplorer/PredicateSelector.tsx diff --git a/src/__mocks__/handlers/DataExplorer/handlers.ts b/src/__mocks__/handlers/DataExplorer/handlers.ts index 5f0b0076d..00018b805 100644 --- a/src/__mocks__/handlers/DataExplorer/handlers.ts +++ b/src/__mocks__/handlers/DataExplorer/handlers.ts @@ -100,6 +100,7 @@ export const getMockResource = ( extra: { [key: string]: any }, project: string = 'hippocampus' ): Resource => ({ + ...extra, '@id': `https://bbp.epfl.ch/neurosciencegraph/data/${selfSuffix}`, '@type': 'https://bbp.epfl.ch/ontologies/core/bmo/SimulationCampaignConfiguration', @@ -115,7 +116,6 @@ export const getMockResource = ( _self: `https://bbp.epfl.ch/nexus/v1/resources/bbp/${project}/_/${selfSuffix}`, _updatedAt: '2023-06-21T09:39:47.844Z', _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/antonel', - ...extra, }); export const defaultMockResult: Resource[] = [ diff --git a/src/shared/App.less b/src/shared/App.less index adf84b82a..7d47123bc 100644 --- a/src/shared/App.less +++ b/src/shared/App.less @@ -32,9 +32,12 @@ } &.data-explorer-container { width: fit-content; - padding-right: 0; + min-width: calc(100vw - 1rem); padding-top: 0; margin-right: 1rem; + height: 100%; + min-height: calc(100vh - 52px); + margin-top: 0; } } diff --git a/src/shared/layouts/FusionMainLayout.less b/src/shared/layouts/FusionMainLayout.less index 3fc93a8a2..736196f42 100644 --- a/src/shared/layouts/FusionMainLayout.less +++ b/src/shared/layouts/FusionMainLayout.less @@ -5,13 +5,18 @@ height: 100%; min-height: 100vh !important; + &.wall { + .fusion-main-layout__content { + margin-top: 0; + } + } .project-panel, .workflow-step-view__panel { left: 80px; } .ant-layout-content { - // margin-top: 52px; + margin-top: 52px; } .ant-menu-item, diff --git a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx new file mode 100644 index 000000000..d455ddb36 --- /dev/null +++ b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx @@ -0,0 +1,160 @@ +import { getAllPaths, isPathMissing } from './PredicateSelector'; + +describe('DataExplorerSpec-Utils', () => { + it('shows all paths for resources', () => { + const resources = [ + { + '@id': '123', + distribution: [ + { name: 'foo', label: ['label1', 'label2'] }, + { filename: 'foo2', label: 'label3' }, + ], + }, + { + '@id': '123', + distribution: [ + { + name: 'foo', + label: ['label1', 'label2'], + contentType: [ + { mimeType: 'application/json' }, + { + mimeType: 'application/csv', + extension: ['csv'], + possibleExtensions: ['2', '2'], + }, + ], + }, + { filename: 'foo2', label: 'label3' }, + ], + contributors: { + name: { firstname: 'Michael', lastname: 'Angelo' }, + }, + }, + ]; + const expectedPaths = [ + '@id', + 'contributors', + 'contributors.name', + 'contributors.name.firstname', + 'contributors.name.lastname', + 'distribution', + 'distribution.contentType', + 'distribution.contentType.extension', + 'distribution.contentType.mimeType', + 'distribution.contentType.possibleExtensions', + 'distribution.filename', + 'distribution.label', + 'distribution.name', + ]; + expect(getAllPaths(resources)).toEqual(expectedPaths); + }); + + it('sorts path starting with underscore at the end of the list', () => { + const resources = [ + { + _author: { name: 'Archer', designation: 'spy' }, + name: 'nameValue', + _createdAt: '12 Jan 2020', + }, + { + _updatedAt: 'some time ago', + name: 'anotherNameValue', + _createdAt: '12 September 2020', + project: 'secret project', + }, + ]; + const expectedPaths = [ + 'name', + 'project', + '_author', + '_author.designation', + '_author.name', + '_createdAt', + '_updatedAt', + ]; + const receivedPaths = getAllPaths(resources); + + expect(receivedPaths).toEqual(expectedPaths); + }); + + it('returns true when top level property does not exist in resource', () => { + const resource = { + foo: 'some value', + nullValue: null, + undefinedValue: undefined, + emptyString: '', + emptyArray: [], + emptyObject: {}, + distribution: [ + { + name: 'sally', + label: { + official: 'official', + unofficial: 'unofficial', + emptyArray: [], + emptyString: '', + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + }, + { + name: 'sally', + sillyname: 'soliloquy', + label: [ + { + official: 'official', + emptyArray: [], + emptyString: '', + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + { + official: 'official', + unofficial: 'unofficial', + emptyArray: [1], + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + ], + }, + ], + }; + expect(isPathMissing(resource, 'bar')).toEqual(true); + expect(isPathMissing(resource, 'nullValue')).toEqual(false); + expect(isPathMissing(resource, 'undefinedValue')).toEqual(false); + expect(isPathMissing(resource, 'emptyString')).toEqual(false); + expect(isPathMissing(resource, 'emptyArray')).toEqual(false); + expect(isPathMissing(resource, 'emptyObject')).toEqual(false); + + expect(isPathMissing(resource, 'foo')).toEqual(false); + expect(isPathMissing(resource, 'foo.xyz')).toEqual(true); + expect(isPathMissing(resource, 'foo.distribution')).toEqual(true); + + expect(isPathMissing(resource, 'distribution')).toEqual(false); + expect(isPathMissing(resource, 'distribution.name')).toEqual(false); + expect(isPathMissing(resource, 'distribution.name.sillyname')).toEqual( + true + ); + expect( + isPathMissing(resource, 'distribution.name.sillyname.pancake') + ).toEqual(true); + expect(isPathMissing(resource, 'distribution.name.label.pancake')).toEqual( + true + ); + expect(isPathMissing(resource, 'distribution.label.unofficial')).toEqual( + true + ); + expect( + isPathMissing(resource, 'distribution.label.extended.prefix') + ).toEqual(false); + expect( + isPathMissing(resource, 'distribution.label.extended.suffix') + ).toEqual(true); + expect(isPathMissing(resource, 'distribution.foo')).toEqual(true); + expect(isPathMissing(resource, 'distribution.emptyArray')).toEqual(true); + expect(isPathMissing(resource, 'distribution.label.emptyArray')).toEqual( + false + ); + expect(isPathMissing(resource, 'distribution.label.emptyString')).toEqual( + true + ); + }); +}); diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index f16c5e821..e9f191feb 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -1,7 +1,7 @@ import { Resource, createNexusClient } from '@bbp/nexus-sdk'; import { NexusProvider } from '@bbp/react-nexus'; import '@testing-library/jest-dom'; -import { RenderResult } from '@testing-library/react'; +import { RenderResult, act, fireEvent, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import { @@ -18,6 +18,7 @@ import { render, screen, waitFor } from '../../utils/testUtil'; import { DataExplorer } from './DataExplorer'; import { AllProjects } from './ProjectSelector'; import { getColumnTitle } from './DataExplorerTable'; +import { DEFAULT_OPTION, getAllPaths } from './PredicateSelector'; describe('DataExplorer', () => { const server = setupServer( @@ -53,14 +54,22 @@ describe('DataExplorer', () => { user = userEvent.setup(); }); - afterEach(() => { + afterEach(async () => { server.resetHandlers(); + await userEvent.click(container); // Close any open dropdowns }); + afterAll(() => { server.resetHandlers(); server.close(); }); + const DropdownSelector = '.ant-select-dropdown'; + const DropdownOptionSelector = 'div.ant-select-item-option-content'; + const PathMenuLabel = 'path-selector'; + const PredicateMenuLabel = 'predicate-selector'; + const ProjectMenuLabel = 'project-filter'; + const expectRowCountToBe = async (expectedRowsCount: number) => { return await waitFor(() => { const rows = visibleTableRows(); @@ -79,17 +88,6 @@ describe('DataExplorer', () => { return header; }; - const expectedHeader = (propName: string) => { - switch (propName) { - case '_project': - return 'PROJECT'; - case '@type': - return 'TYPE'; - default: - return propName; - } - }; - const getTextForColumn = async (resource: Resource, colName: string) => { const selfCell = await screen.getAllByText( new RegExp(resource._self, 'i'), @@ -113,12 +111,6 @@ describe('DataExplorer', () => { return projectAutocomplete; }; - const selectProject = async (projectName: string) => { - await openProjectAutocomplete(); - const unhcrProject = await getProjectOption(projectName); - await userEvent.click(unhcrProject, { pointerEventsCheck: 0 }); - }; - const searchForProject = async (searchTerm: string) => { const projectAutocomplete = await openProjectAutocomplete(); await userEvent.clear(projectAutocomplete); @@ -149,6 +141,56 @@ describe('DataExplorer', () => { }); }; + const getDropdownOption = async (optionLabel: string) => + await screen.getByText(new RegExp(`${optionLabel}$`), { + selector: DropdownOptionSelector, + }); + + const getRowsForNextPage = async (resources: Resource[]) => { + server.use(dataExplorerPageHandler(resources)); + + const pageInput = await screen.getByRole('listitem', { name: '2' }); + expect(pageInput).toBeInTheDocument(); + + await user.click(pageInput); + + await expectRowCountToBe(3); + }; + + const openMenuFor = async (ariaLabel: string) => { + const menuInput = await screen.getByLabelText(ariaLabel, { + selector: 'input', + }); + await userEvent.click(menuInput, { pointerEventsCheck: 0 }); + await act(async () => { + fireEvent.mouseDown(menuInput); + }); + const menuDropdown = document.querySelector(DropdownSelector); + expect(menuDropdown).toBeInTheDocument(); + return menuDropdown; + }; + + const selectOptionFromMenu = async ( + menuAriaLabel: string, + optionLabel: string + ) => { + await openMenuFor(menuAriaLabel); + const option = await getDropdownOption(optionLabel); + await userEvent.click(option, { pointerEventsCheck: 0 }); + }; + + /** + * @returns All options visible in the currently open dropdown menu in the DOM. + * NOTE: Since antd menus use virtual scroll, not all options inside the menu are visible. + * This function only returns those options that are visible. + */ + const getVisibleOptionsFromMenu = () => { + const menuDropdown = document.querySelector(DropdownSelector); + return Array.from( + menuDropdown?.querySelectorAll(DropdownOptionSelector) ?? [] + ); + }; + it('shows rows for all fetched resources', async () => { await expectRowCountToBe(10); }); @@ -161,7 +203,7 @@ describe('DataExplorer', () => { for (const topLevelProperty of Object.keys(mockResource)) { if (!seenColumns.has(topLevelProperty)) { seenColumns.add(topLevelProperty); - const header = expectedHeader(topLevelProperty); + const header = getColumnTitle(topLevelProperty); await expectColumHeaderToExist(header); } } @@ -187,20 +229,13 @@ describe('DataExplorer', () => { it('updates columns when new page is selected', async () => { await expectRowCountToBe(10); - server.use( - dataExplorerPageHandler([ - getMockResource('self1', { author: 'piggy', edition: 1 }), - getMockResource('self2', { author: ['iggy', 'twinky'] }), - getMockResource('self3', { year: 2013 }), - ]) - ); + const mockResourcesForNextPage = [ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]; - const pageInput = await screen.getByRole('listitem', { name: '2' }); - expect(pageInput).toBeInTheDocument(); - - await user.click(pageInput); - - await expectRowCountToBe(3); + await getRowsForNextPage(mockResourcesForNextPage); await expectColumHeaderToExist('author'); await expectColumHeaderToExist('edition'); @@ -310,19 +345,14 @@ describe('DataExplorer', () => { expect(textForSpecialProperty).toEqual('{}'); }); - const getProjectOption = async (projectName: string) => - await screen.getByText(new RegExp(projectName, 'i'), { - selector: 'div.ant-select-item-option-content', - }); - it('shows resources filtered by the selected project', async () => { - await selectProject('unhcr'); + await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); visibleTableRows().forEach(row => expect(projectFromRow(row)).toMatch(/unhcr/i) ); - await selectProject(AllProjects); + await selectOptionFromMenu(ProjectMenuLabel, AllProjects); await expectRowCountToBe(10); }); @@ -333,4 +363,44 @@ describe('DataExplorer', () => { await searchForProject('bbc'); await expectProjectOptionsToMatch('bbc'); }); + + it('shows paths as options in a select menu of path selector', async () => { + await openMenuFor('path-selector'); + + const pathOptions = getVisibleOptionsFromMenu(); + + const expectedPaths = getAllPaths(defaultMockResult); + expect(expectedPaths.length).toBeGreaterThanOrEqual( + Object.keys(defaultMockResult[0]).length + ); + expect(pathOptions[0].innerHTML).toMatch(DEFAULT_OPTION); + + pathOptions.slice(1).forEach((path, index) => { + expect(path.innerHTML).toMatch( + new RegExp(`${expectedPaths[index]}$`, 'i') + ); + }); + + expect(pathOptions.length).toBeGreaterThanOrEqual(3); // Since antd options in a select menu are displayed in a virtual list (by default), not all expected options are in the DOM. + }); + + it('shows resources that have path missing', async () => { + await expectRowCountToBe(10); + const mockResourcesForNextPage = [ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]; + + await getRowsForNextPage(mockResourcesForNextPage); + await expectRowCountToBe(3); + + await selectOptionFromMenu(PathMenuLabel, 'author'); + await selectOptionFromMenu(PredicateMenuLabel, 'Empty value'); + await expectRowCountToBe(1); + + await selectOptionFromMenu(PathMenuLabel, 'edition'); + await selectOptionFromMenu(PredicateMenuLabel, 'Empty value'); + await expectRowCountToBe(2); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index d5654adfa..6b02c418e 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -1,32 +1,41 @@ import { Resource } from '@bbp/nexus-sdk'; import { useNexusContext } from '@bbp/react-nexus'; import { notification } from 'antd'; -import { isObject, isString } from 'lodash'; +import { isString } from 'lodash'; import React, { useReducer } from 'react'; import { useQuery } from 'react-query'; import { getResourceLabel } from '../../shared/utils'; import { DataExplorerTable } from './DataExplorerTable'; import './styles.less'; import { ProjectSelector } from './ProjectSelector'; +import { PredicateSelector, isPathMissing } from './PredicateSelector'; export interface DataExplorerConfiguration { pageSize: number; offset: number; orgAndProject?: [string, string]; + predicatePath: string | null; + predicateFilter: string | null; } export const DataExplorer: React.FC<{}> = () => { const nexus = useNexusContext(); const [ - { pageSize, offset, orgAndProject }, + { pageSize, offset, orgAndProject, predicatePath, predicateFilter }, updateTableConfiguration, ] = useReducer( ( previous: DataExplorerConfiguration, next: Partial ) => ({ ...previous, ...next }), - { pageSize: 50, offset: 0, orgAndProject: undefined } + { + pageSize: 50, + offset: 0, + orgAndProject: undefined, + predicatePath: null, + predicateFilter: null, + } ); const { data: resources, isLoading } = useQuery({ @@ -55,13 +64,14 @@ export const DataExplorer: React.FC<{}> = () => { }, }); - const dataSource: Resource[] = - resources?._results?.map(resource => { - return { - ...resource, - name: getResourceLabel(resource), - }; - }) || []; + const currentPageDataSource: Resource[] = resources?._results || []; + + const displayedDataSource = + predicatePath && predicateFilter + ? currentPageDataSource.filter(resource => + isPathMissing(resource, predicatePath) + ) + : currentPageDataSource; return (
@@ -77,10 +87,15 @@ export const DataExplorer: React.FC<{}> = () => { } }} /> +
= () => {
); }; + +export const isObject = (value: any) => { + return typeof value === 'object' && value !== null && !Array.isArray(value); +}; + +export const columnsFromDataSource = (resources: Resource[]): string[] => { + const columnNames = new Set(); + + resources.forEach(resource => { + Object.keys(resource).forEach(key => columnNames.add(key)); + }); + + return Array.from(columnNames); +}; diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index d5a091842..0f4a4b9c0 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -16,6 +16,7 @@ interface TDataExplorerTable { pageSize: number; offset: number; updateTableConfiguration: React.Dispatch>; + columns: string[]; } type TColumnNameToConfig = Map>; @@ -23,6 +24,7 @@ type TColumnNameToConfig = Map>; export const DataExplorerTable: React.FC = ({ isLoading, dataSource, + columns, total, pageSize, offset, @@ -47,44 +49,40 @@ export const DataExplorerTable: React.FC = ({ }; return ( - <> - - columns={dynamicColumnsForDataSource(dataSource)} - dataSource={dataSource} - rowKey={record => record._self} - loading={isLoading} - bordered={false} - className="data-explorer-table" - rowClassName="data-explorer-row" - scroll={{ x: 'max-content', y: '100vh' }} - locale={{ - emptyText() { - return isLoading ? <> : ; - }, - }} - pagination={tablePaginationConfig} - /> - + + columns={columnsConfig(columns)} + dataSource={dataSource} + rowKey={record => record._self} + loading={isLoading} + bordered={false} + className="data-explorer-table" + rowClassName="data-explorer-row" + scroll={{ x: 'max-content' }} + locale={{ + emptyText() { + return isLoading ? <> : ; + }, + }} + pagination={tablePaginationConfig} + /> ); }; /** * 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). */ -const dynamicColumnsForDataSource = ( - resources: Resource[] +export const columnsConfig = ( + columnNames: string[] ): ColumnType[] => { - const colNameToConfig = new Map(initialTableConfig()); - - resources.forEach(resource => { - const newResourceKeys = Object.keys(resource).filter( - key => !colNameToConfig.has(key) - ); + const colNameToConfig = new Map( + columnNames.length === 0 ? [] : initialTableConfig() + ); - newResourceKeys.forEach(key => { - colNameToConfig.set(key, { ...defaultColumnConfig(key) }); - }); - }); + for (const columnName of columnNames) { + if (!colNameToConfig.has(columnName)) { + colNameToConfig.set(columnName, { ...defaultColumnConfig(columnName) }); + } + } return Array.from(colNameToConfig.values()); }; diff --git a/src/subapps/dataExplorer/PredicateSelector.tsx b/src/subapps/dataExplorer/PredicateSelector.tsx new file mode 100644 index 000000000..209e0f53a --- /dev/null +++ b/src/subapps/dataExplorer/PredicateSelector.tsx @@ -0,0 +1,132 @@ +import { Select } from 'antd'; +import React from 'react'; +import { DataExplorerConfiguration, isObject } from './DataExplorer'; +import './styles.less'; +import { Resource } from '@bbp/nexus-sdk'; + +interface Props { + dataSource: Resource[]; + onPredicateChange: React.Dispatch>; +} + +export const PredicateSelector: React.FC = ({ + dataSource, + onPredicateChange, +}: Props) => { + const pathOptions = [ + { value: DEFAULT_OPTION }, + ...getAllPaths(dataSource).map(path => ({ value: path })), + ]; + const predicateOptions = [ + { value: DEFAULT_OPTION }, + { value: 'Empty value' }, + ]; + + return ( +
+ with + + { + onPredicateChange({ + predicateFilter: + predicateLabel === DEFAULT_OPTION ? null : predicateLabel, + }); + }} + aria-label="predicate-selector" + style={{ width: 200 }} + className="select-menu" + popupClassName="search-menu" + allowClear={true} + onClear={() => onPredicateChange({ predicateFilter: null })} + /> +
+ ); +}; + +export const DEFAULT_OPTION = '-'; + +export const pathOptions = (paths: string[]) => [ + { value: DEFAULT_OPTION }, + paths.map(path => ({ value: path })), +]; + +const UNDERSCORE = '_'; + +export const getAllPaths = (objects: { [key: string]: any }[]): string[] => { + return Array.from(getPathsForResource(objects, '')).sort( + (a: string, b: string) => { + // Sorts paths alphabetically. Additionally all paths starting with an underscore are sorted at the end of the list (because they represent metadata). + if (a.startsWith(UNDERSCORE) && b.startsWith(UNDERSCORE)) { + return a.localeCompare(b); + } + if (a.startsWith(UNDERSCORE)) { + return 1; + } + if (b.startsWith(UNDERSCORE)) { + return -1; + } + return a.localeCompare(b); + } + ); +}; + +const getPathsForResource = ( + resource: { [key: string]: any } | { [key: string]: any }[], + currentPath: string, + paths: Set = new Set() +) => { + if (Array.isArray(resource)) { + resource.forEach(res => getPathsForResource(res, currentPath, paths)); + } else if (typeof resource === 'object' && resource !== null) { + const keys = Object.keys(resource); + for (const key of keys) { + const path = currentPath ? `${currentPath}.${key}` : `${key}`; + paths.add(path); + getPathsForResource(resource[key], path, paths); + } + } + return paths; +}; + +export const isPathMissing = ( + resource: { [key: string]: any }, + path: string +): boolean => { + if (path in resource && path !== '') { + return false; + } + + const subpaths = path.split('.'); + + for (const subpath of subpaths) { + if (!(subpath in resource)) { + return true; + } + const valueAtSubpath = resource[subpath]; + const remainingPath = subpaths.slice(1); + if (Array.isArray(valueAtSubpath)) { + return valueAtSubpath.some(value => + isPathMissing(value, remainingPath.join('.')) + ); + } + if (isObject(valueAtSubpath)) { + return isPathMissing(valueAtSubpath, remainingPath.join('.')); + } + } + return true; +}; diff --git a/src/subapps/dataExplorer/ProjectSelector.tsx b/src/subapps/dataExplorer/ProjectSelector.tsx index c6a0fc1d2..e1e19e0bc 100644 --- a/src/subapps/dataExplorer/ProjectSelector.tsx +++ b/src/subapps/dataExplorer/ProjectSelector.tsx @@ -19,7 +19,7 @@ export const ProjectSelector: React.FC = ({ onSelect }: Props) => { ].filter(project => project.value.includes(searchTerm)); return ( -
+
Show me tr > th.data-explorer-column { background-color: #f5f5f5 !important; font-family: 'Titillium Web'; @@ -83,3 +100,9 @@ border-bottom: 1px solid #d9d9d9; } } + +.search-menu { + .ant-select-item-option-content { + color: @fusion-blue-8; + } +} From effed7b89bb1490accb0a1aab33132527bcd874f Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 4 Jul 2023 17:29:18 +0200 Subject: [PATCH 071/192] de-12: refactor resource array constructor --- .../components/ResourceEditor/editorUtils.ts | 57 +++++++++---------- src/shared/containers/ResourceEditor.tsx | 15 ++--- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index cf3f26de6..3d1804c32 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -13,6 +13,7 @@ import { getOrgAndProjectFromResourceObject, getResourceLabel, } from '../../utils'; +import { TDEResource } from '../../store/reducers/data-explorer'; import { UISettingsActionTypes, TUpdateJSONEditorPopoverAction, @@ -72,7 +73,25 @@ export const getNormalizedTypes = (types?: string | string[]) => { const mayBeResolvableLink = (url: string): boolean => { return isValidUrl(url) && !isUrlCurieFormat(url) && !isStorageLink(url); }; - +export const getDataExplorerResourceItemArray = ( + entity: { orgLabel: string; projectLabel: string }, + data: Resource +) => { + return (isDownloadableLink(data) && data._mediaType + ? [ + entity?.orgLabel, + entity?.projectLabel, + data['@id'], + data._rev, + data._mediaType, + ] + : [ + entity?.orgLabel, + entity?.projectLabel, + data['@id'], + data._rev, + ]) as TDEResource; +}; export async function resolveLinkInEditor({ nexus, dispatch, @@ -111,20 +130,10 @@ export async function resolveLinkInEditor({ _self: data._self, title: getResourceLabel(data), types: getNormalizedTypes(data['@type']), - resource: isDownloadable - ? [ - entity?.orgLabel, - entity?.projectLabel, - data['@id'], - data._rev, - data._mediaType, - ] - : [ - entity?.orgLabel, - entity?.projectLabel, - data['@id'], - data._rev, - ], + resource: getDataExplorerResourceItemArray( + entity ?? { orgLabel: '', projectLabel: '' }, + data + ), }, }, }); @@ -156,20 +165,10 @@ export async function resolveLinkInEditor({ _self: item._self, title: getResourceLabel(item), types: getNormalizedTypes(item['@type']), - resource: isDownloadable - ? [ - entity?.orgLabel, - entity?.projectLabel, - item['@id'], - item._rev, - item._mediaType, - ] - : [ - entity?.orgLabel, - entity?.projectLabel, - item['@id'], - item._rev, - ], + resource: getDataExplorerResourceItemArray( + entity ?? { orgLabel: '', projectLabel: '' }, + item + ), }; }), }, diff --git a/src/shared/containers/ResourceEditor.tsx b/src/shared/containers/ResourceEditor.tsx index 615ae87d8..e1b9501bf 100644 --- a/src/shared/containers/ResourceEditor.tsx +++ b/src/shared/containers/ResourceEditor.tsx @@ -9,7 +9,10 @@ import { useHistory, useLocation } from 'react-router'; import { useDispatch } from 'react-redux'; import ResourceEditor from '../components/ResourceEditor'; import { useNexusContext } from '@bbp/react-nexus'; -import { getNormalizedTypes } from '../components/ResourceEditor/editorUtils'; +import { + getDataExplorerResourceItemArray, + getNormalizedTypes, +} from '../components/ResourceEditor/editorUtils'; import useNotification, { parseNexusError } from '../hooks/useNotification'; import { InitDataExplorerGraphFlowLimitedVersion, @@ -132,12 +135,10 @@ const ResourceEditorContainer: React.FunctionComponent<{ _self: data._self, types: getNormalizedTypes(data['@type']), title: getResourceLabel(data), - resource: [ - orgProject?.orgLabel ?? '', - orgProject?.projectLabel ?? '', - data['@id'], - data._rev, - ], + resource: getDataExplorerResourceItemArray( + orgProject ?? { orgLabel: '', projectLabel: '' }, + data + ), }, limited: true, }) From be3b5e3f056224de62fd6f94fc7791de48f9291a Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 4 Jul 2023 17:41:45 +0200 Subject: [PATCH 072/192] de-12: use getMockResource --- .../DataExplorerGraphFlow/handlers.ts | 281 +++++++++--------- 1 file changed, 134 insertions(+), 147 deletions(-) diff --git a/src/__mocks__/handlers/DataExplorerGraphFlow/handlers.ts b/src/__mocks__/handlers/DataExplorerGraphFlow/handlers.ts index 0da6082f3..df9c98f91 100644 --- a/src/__mocks__/handlers/DataExplorerGraphFlow/handlers.ts +++ b/src/__mocks__/handlers/DataExplorerGraphFlow/handlers.ts @@ -1,5 +1,6 @@ import { rest } from 'msw'; import { deltaPath } from '__mocks__/handlers/handlers'; +import { getMockResource } from '../DataExplorer/handlers'; const resource = { '@context': [ @@ -166,170 +167,156 @@ const resource = { _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/cgonzale', }; -const initialResource = { - '@context': [ - 'https://bluebrain.github.io/nexus/contexts/metadata.json', - 'https://bbp.neuroshapes.org', - ], - '@id': - 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/bfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', - '@type': ['Entity', 'Dataset', 'NeuronMorphology', 'ReconstructedCell'], - annotation: { - '@type': ['MTypeAnnotation', 'Annotation'], - hasBody: { - '@id': 'ilx:0383236', - '@type': ['MType', 'AnnotationBody'], - label: 'L6_SBC', - }, - name: 'M-type Annotation', - }, - brainLocation: { - '@type': 'BrainLocation', - brainRegion: { - '@id': 'uberon:0008933', - label: 'primary somatosensory cortex', - }, - layer: { - '@id': 'uberon:0005395', - label: 'layer 6', - }, - }, - contribution: [ - { - '@type': 'Contribution', - agent: { - '@id': 'https://orcid.org/0000-0001-9358-1315', - '@type': 'Agent', - }, - hadRole: { - '@id': 'Neuron:ElectrophysiologyRecordingRole', - label: 'neuron electrophysiology recording role', +const initialResource = getMockResource( + 'neuronmorphologies/bfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', + { + '@context': [ + 'https://bluebrain.github.io/nexus/contexts/metadata.json', + 'https://bbp.neuroshapes.org', + ], + '@type': ['Entity', 'Dataset', 'NeuronMorphology', 'ReconstructedCell'], + annotation: { + '@type': ['MTypeAnnotation', 'Annotation'], + hasBody: { + '@id': 'ilx:0383236', + '@type': ['MType', 'AnnotationBody'], + label: 'L6_SBC', }, + name: 'M-type Annotation', }, - { - '@type': 'Contribution', - agent: { - '@id': 'https://www.grid.ac/institutes/grid.5333.6', - '@type': 'Agent', + brainLocation: { + '@type': 'BrainLocation', + brainRegion: { + '@id': 'uberon:0008933', + label: 'primary somatosensory cortex', + }, + layer: { + '@id': 'uberon:0005395', + label: 'layer 6', }, }, - ], - derivation: { - '@type': 'Derivation', - entity: { - '@id': - 'https://bbp.epfl.ch/neurosciencegraph/data/350bcafe-9cbb-4c15-bad3-1caed2cbb990', - '@type': ['PatchedCell', 'Entity'], - }, - }, - description: - 'This dataset is about an in vitro-filled neuron morphology from layer 6 with m-type L6_SBC. The distribution contains the neuron morphology in ASC and in SWC file format.', - distribution: [ - { - '@type': 'DataDownload', - atLocation: { - '@type': 'Location', - location: - 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/a/a/e/d/8/2/b/5/tkb060126a2_ch3_bc_n_jh_100x_1.asc', - store: { - '@id': - 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', - '@type': 'RemoteDiskStorage', - _rev: 1, + contribution: [ + { + '@type': 'Contribution', + agent: { + '@id': 'https://orcid.org/0000-0001-9358-1315', + '@type': 'Agent', + }, + hadRole: { + '@id': 'Neuron:ElectrophysiologyRecordingRole', + label: 'neuron electrophysiology recording role', }, }, - contentSize: { - unitCode: 'bytes', - value: 1097726, + { + '@type': 'Contribution', + agent: { + '@id': 'https://www.grid.ac/institutes/grid.5333.6', + '@type': 'Agent', + }, }, - contentUrl: - 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2Fbf146eaf-48cf-4b83-b375-bbb92ce7f7c0', - digest: { - algorithm: 'SHA-256', - value: - 'efcf3d6660d9769b3f3066e874c8f13536fbc398b5605ffc5acc223884362ff6', + ], + derivation: { + '@type': 'Derivation', + entity: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/350bcafe-9cbb-4c15-bad3-1caed2cbb990', + '@type': ['PatchedCell', 'Entity'], }, - encodingFormat: 'application/asc', - name: 'tkb060126a2_ch3_bc_n_jh_100x_1.asc', }, - { - '@type': 'DataDownload', - atLocation: { - '@type': 'Location', - location: - 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/6/4/3/8/3/d/0/3/tkb060126a2_ch3_bc_n_jh_100x_1.swc', - store: { - '@id': - 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', - '@type': 'RemoteDiskStorage', - _rev: 1, + description: + 'This dataset is about an in vitro-filled neuron morphology from layer 6 with m-type L6_SBC. The distribution contains the neuron morphology in ASC and in SWC file format.', + distribution: [ + { + '@type': 'DataDownload', + atLocation: { + '@type': 'Location', + location: + 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/a/a/e/d/8/2/b/5/tkb060126a2_ch3_bc_n_jh_100x_1.asc', + store: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', + '@type': 'RemoteDiskStorage', + _rev: 1, + }, + }, + contentSize: { + unitCode: 'bytes', + value: 1097726, + }, + contentUrl: + 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2Fbf146eaf-48cf-4b83-b375-bbb92ce7f7c0', + digest: { + algorithm: 'SHA-256', + value: + 'efcf3d6660d9769b3f3066e874c8f13536fbc398b5605ffc5acc223884362ff6', }, + encodingFormat: 'application/asc', + name: 'tkb060126a2_ch3_bc_n_jh_100x_1.asc', }, - contentSize: { - unitCode: 'bytes', - value: 891821, + { + '@type': 'DataDownload', + atLocation: { + '@type': 'Location', + location: + 'file:///gpfs/bbp.cscs.ch/data/project/proj109/nexus/c7d70522-4305-480a-b190-75d757ed9a49/6/4/3/8/3/d/0/3/tkb060126a2_ch3_bc_n_jh_100x_1.swc', + store: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/4820323e-bee0-48d2-824f-9d9d404dbbee', + '@type': 'RemoteDiskStorage', + _rev: 1, + }, + }, + contentSize: { + unitCode: 'bytes', + value: 891821, + }, + contentUrl: + 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2F60025362-1ca8-425e-908c-a01e4661c3e7', + digest: { + algorithm: 'SHA-256', + value: + '22bac983b129fe806c80a9ddb4dcf77b79c1a6a28adffd6674290fb1f014a30e', + }, + encodingFormat: 'application/swc', + name: 'tkb060126a2_ch3_bc_n_jh_100x_1.swc', }, - contentUrl: - 'https://bbp.epfl.ch/nexus/v1/files/public/sscx/https%3A%2F%2Fbbp.epfl.ch%2Fneurosciencegraph%2Fdata%2F60025362-1ca8-425e-908c-a01e4661c3e7', - digest: { - algorithm: 'SHA-256', - value: - '22bac983b129fe806c80a9ddb4dcf77b79c1a6a28adffd6674290fb1f014a30e', + ], + generation: { + '@type': 'Generation', + activity: { + '@id': + 'https://bbp.epfl.ch/neurosciencegraph/data/9ad281da-e352-4275-b1fa-6a3516a654c9', + '@type': ['Activity', 'Reconstruction'], }, - encodingFormat: 'application/swc', - name: 'tkb060126a2_ch3_bc_n_jh_100x_1.swc', }, - ], - generation: { - '@type': 'Generation', - activity: { + isPartOf: { '@id': - 'https://bbp.epfl.ch/neurosciencegraph/data/9ad281da-e352-4275-b1fa-6a3516a654c9', - '@type': ['Activity', 'Reconstruction'], + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/23d3d87e-94fe-4639-b5c8-a26a712587e6', + '@type': 'Entity', }, - }, - isPartOf: { - '@id': - 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/23d3d87e-94fe-4639-b5c8-a26a712587e6', - '@type': 'Entity', - }, - license: { - '@id': 'https://creativecommons.org/licenses/by/4.0/', - '@type': 'License', - }, - name: 'tkb060126a2_ch3_bc_n_jh_100x_1', - objectOfStudy: { - '@id': - 'http://bbp.epfl.ch/neurosciencegraph/taxonomies/objectsofstudy/singlecells', - '@type': 'ObjectOfStudy', - label: 'Single Cell', - }, - sameAs: - 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/431a1196-47b5-41a2-931a-3577be9a2dc4', - subject: { - '@type': 'Subject', - species: { - '@id': 'NCBITaxon:10116', - label: 'Rattus norvegicus', + license: { + '@id': 'https://creativecommons.org/licenses/by/4.0/', + '@type': 'License', }, - }, - _constrainedBy: 'https://neuroshapes.org/dash/neuronmorphology', - _createdAt: '2021-11-23T11:34:00.952Z', - _createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/akkaufma', - _deprecated: false, - _incoming: - 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf/incoming', - _outgoing: - 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf/outgoing', - _project: 'https://bbp.epfl.ch/nexus/v1/projects/public/sscx', - _rev: 2, - _schemaProject: - 'https://bbp.epfl.ch/nexus/v1/projects/neurosciencegraph/datamodels', - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/public/sscx/datashapes:neuronmorphology/neuronmorphologies%2Fbfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', - _updatedAt: '2023-06-23T07:34:56.011Z', - _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/cgonzale', -}; + name: 'tkb060126a2_ch3_bc_n_jh_100x_1', + objectOfStudy: { + '@id': + 'http://bbp.epfl.ch/neurosciencegraph/taxonomies/objectsofstudy/singlecells', + '@type': 'ObjectOfStudy', + label: 'Single Cell', + }, + sameAs: + 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/431a1196-47b5-41a2-931a-3577be9a2dc4', + subject: { + '@type': 'Subject', + species: { + '@id': 'NCBITaxon:10116', + label: 'Rattus norvegicus', + }, + }, + } +); + const initialResourceExpanded = { '@id': 'https://bbp.epfl.ch/neurosciencegraph/data/neuronmorphologies/bfdd4d1a-8b06-46fe-b663-7d9f8020dcaf', From 3c29106ab8b38932e8144a2b1d3b2be333c942fb Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 5 Jul 2023 09:13:29 +0200 Subject: [PATCH 073/192] fix: use react router useLocation instead window location in back btn --- .../canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx | 4 ++-- .../{NavigationBack.tsx => NavigationBackButton.tsx} | 3 ++- .../molecules/DataExplorerGraphFlowMolecules/index.ts | 2 +- .../NavigationStack.spec.tsx | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) rename src/shared/molecules/DataExplorerGraphFlowMolecules/{NavigationBack.tsx => NavigationBackButton.tsx} (89%) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index 18f1ab33d..e51e0aa5a 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -9,7 +9,7 @@ import { ResetDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; import { - NavigationBack, + NavigationBackButton, NavigationCollapseButton, } from '../../molecules/DataExplorerGraphFlowMolecules'; import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; @@ -81,7 +81,7 @@ const DataExplorerGraphFlow = () => {
- +
diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx similarity index 89% rename from src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx rename to src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx index a36b047d9..9d6a93c65 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBack.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { useHistory } from 'react-router'; +import { useHistory, useLocation } from 'react-router'; import { ArrowLeftOutlined } from '@ant-design/icons'; import { ReturnBackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; @@ -9,6 +9,7 @@ import './styles.less'; const NavigationBack = () => { const dispatch = useDispatch(); const history = useHistory(); + const location = useLocation(); const { links } = useSelector((state: RootState) => state.dataExplorer); const onBack = () => { history.replace(location.pathname); diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index 925d6bafb..edc413476 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1,5 +1,5 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; -export { default as NavigationBack } from './NavigationBack'; +export { default as NavigationBackButton } from './NavigationBackButton'; export { default as NavigationStackItem } from './NavigationStackItem'; export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; export { default as NavigationCollapseButton } from './NavigationCollapseButton'; diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index a891e1a45..a035a80c7 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -12,7 +12,7 @@ import { NexusProvider } from '@bbp/react-nexus'; import { deltaPath } from '../../../__mocks__/handlers/handlers'; import NavigationStack from './NavigationStack'; import { - NavigationBack, + NavigationBackButton, NavigationCollapseButton, } from '../../molecules/DataExplorerGraphFlowMolecules'; import configureStore from '../../store'; @@ -108,7 +108,7 @@ describe('NavigationStack', () => { <> - + @@ -403,7 +403,7 @@ describe('NavigationStack', () => { it('should decode the navigation digest at first render', () => { store.dispatch(ResetDataExplorerGraphFlow({ initialState: null })); store.dispatch(PopulateDataExplorerGraphFlow(sampleDigest)); - render(app); + rerender(app); const dataExplorerState = store.getState().dataExplorer; expect(dataExplorerState.links.length).toBe(2); expect(dataExplorerState.current._self).toBe(digestCurrentSelf); From 3bcdb32a5babf3908532f60ebd23f734857072b8 Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Fri, 30 Jun 2023 16:02:31 +0200 Subject: [PATCH 074/192] US 6 // Show resources that contain a specific value Signed-off-by: Dinika Saxena --- src/shared/hooks/useAccessDataForTable.tsx | 3 +- .../dataExplorer/DataExplorer-utils.spec.tsx | 146 +++++++++++++++++- .../dataExplorer/DataExplorer.spec.tsx | 43 ++++++ src/subapps/dataExplorer/DataExplorer.tsx | 40 ++++- .../dataExplorer/PredicateSelector.tsx | 94 +++++++++-- src/subapps/dataExplorer/predicate-filter.svg | 5 + src/subapps/dataExplorer/styles.less | 40 +++++ src/utils/stringUtils.ts | 1 + 8 files changed, 353 insertions(+), 19 deletions(-) create mode 100644 src/subapps/dataExplorer/predicate-filter.svg create mode 100644 src/utils/stringUtils.ts diff --git a/src/shared/hooks/useAccessDataForTable.tsx b/src/shared/hooks/useAccessDataForTable.tsx index abae5ac05..57ad4e773 100644 --- a/src/shared/hooks/useAccessDataForTable.tsx +++ b/src/shared/hooks/useAccessDataForTable.tsx @@ -47,6 +47,7 @@ import { addColumnsForES, rowRender } from '../utils/parseESResults'; import { sparqlQueryExecutor } from '../utils/querySparqlView'; import { CartContext } from './useDataCart'; import PromisePool from '@supercharge/promise-pool'; +import { normalizeString } from '../../utils/stringUtils'; export const EXPORT_CSV_FILENAME = 'nexus-query-result.csv'; export const CSV_MEDIATYPE = 'text/csv'; @@ -126,8 +127,6 @@ type ColumnSorter = ( b: Record ) => -1 | 1 | 0; -const normalizeString = (str: string) => str.trim().toLowerCase(); - const sorter = (dataIndex: string): ColumnSorter => { return ( a: { diff --git a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx index d455ddb36..d13a57358 100644 --- a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx @@ -1,4 +1,8 @@ -import { getAllPaths, isPathMissing } from './PredicateSelector'; +import { + doesResourceContain, + getAllPaths, + isPathMissing, +} from './PredicateSelector'; describe('DataExplorerSpec-Utils', () => { it('shows all paths for resources', () => { @@ -157,4 +161,144 @@ describe('DataExplorerSpec-Utils', () => { true ); }); + + it('checks if array strings can be checked for contains', () => { + const resource = { + '@id': + 'https://bluebrain.github.io/nexus/vocabulary/defaultElasticSearchIndex', + '@type': ['ElasticSearchView', 'View'], + }; + expect(doesResourceContain(resource, '@type', '')).toEqual(true); + expect(doesResourceContain(resource, '@type', 'ElasticSearchView')).toEqual( + true + ); + expect(doesResourceContain(resource, '@type', 'File')).toEqual(false); + }); + + it('checks if path has a specific value', () => { + const resource = { + foo: 'some value', + bar: 42, + distribution: [ + { + name: 'sally', + filename: 'billy', + label: { + official: 'official', + unofficial: 'rebel', + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + }, + { + name: 'sally', + sillyname: 'soliloquy', + filename: 'bolly', + label: [ + { + official: 'official', + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + { + official: 'official', + unofficial: 'rebel', + extended: [{ prefix: 1, suffix: '2' }, { prefix: '1' }], + }, + ], + }, + ], + }; + expect(doesResourceContain(resource, 'foo', '')).toEqual(true); + expect(doesResourceContain(resource, 'foo', 'some value')).toEqual(true); + expect(doesResourceContain(resource, 'foo', '2')).toEqual(false); + expect(doesResourceContain(resource, 'bar', '42')).toEqual(true); + expect(doesResourceContain(resource, 'distribution.name', 'sally')).toEqual( + true + ); + expect( + doesResourceContain(resource, 'distribution.sillyname', 'sally') + ).toEqual(false); + expect( + doesResourceContain(resource, 'distribution.filename', 'billy') + ).toEqual(true); + expect( + doesResourceContain(resource, 'distribution.label', 'madeUpLabel') + ).toEqual(false); + expect( + doesResourceContain(resource, 'distribution.official', 'official') + ).toEqual(false); + expect( + doesResourceContain(resource, 'distribution.label.official', 'official') + ).toEqual(true); + expect( + doesResourceContain(resource, 'distribution.label.unofficial', 'official') + ).toEqual(false); + expect( + doesResourceContain(resource, 'distribution.label.unofficial', 'rebel') + ).toEqual(true); + expect( + doesResourceContain(resource, 'distribution.label.extended.prefix', '1') + ).toEqual(true); + expect( + doesResourceContain(resource, 'distribution.label.extended.prefix', '10') + ).toEqual(false); + expect( + doesResourceContain(resource, 'distribution.label.extended.suffix', '1') + ).toEqual(false); + expect( + doesResourceContain(resource, 'distribution.label.extended.suffix', '2') + ).toEqual(true); + expect( + doesResourceContain( + resource, + 'distribution.label.extended.nonexisting', + '2' + ) + ).toEqual(false); + }); + + it('ignores case when checking for contains value', () => { + const resource = { + distribution: [ + { + name: 'sally', + filename: 'billy', + label: ['ChiPmunK'], + }, + { + name: 'sally', + sillyname: 'soliloquy', + filename: 'bolly', + }, + ], + }; + expect( + doesResourceContain(resource, 'distribution.filename', 'BiLLy') + ).toEqual(true); + expect( + doesResourceContain(resource, 'distribution.filename', 'Lilly') + ).toEqual(false); + expect( + doesResourceContain(resource, 'distribution.label', 'chipmunk') + ).toEqual(true); + }); + + it('checks if value is a substring in existing path when checking for contains', () => { + const resource = { + distribution: [ + { + name: 'sally', + filename: 'billy', + label: ['ChiPmunK'], + }, + { + name: 'sally', + sillyname: 'soliloquy', + filename: 'bolly', + }, + ], + }; + expect( + doesResourceContain(resource, 'distribution.filename', 'lly') + ).toEqual(true); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index e9f191feb..501a8a662 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -73,6 +73,7 @@ describe('DataExplorer', () => { const expectRowCountToBe = async (expectedRowsCount: number) => { return await waitFor(() => { const rows = visibleTableRows(); + rows.forEach(row => console.log('Inner html', row.innerHTML)); expect(rows.length).toEqual(expectedRowsCount); return rows; }); @@ -175,6 +176,7 @@ describe('DataExplorer', () => { optionLabel: string ) => { await openMenuFor(menuAriaLabel); + console.log('Lookig for option ', optionLabel, ' in menu ', menuAriaLabel); const option = await getDropdownOption(optionLabel); await userEvent.click(option, { pointerEventsCheck: 0 }); }; @@ -403,4 +405,45 @@ describe('DataExplorer', () => { await selectOptionFromMenu(PredicateMenuLabel, 'Empty value'); await expectRowCountToBe(2); }); + + it('shows resources that contains value provided by user', async () => { + await expectRowCountToBe(10); + const mockResourcesForNextPage = [ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]; + + await getRowsForNextPage(mockResourcesForNextPage); + await expectRowCountToBe(3); + + await selectOptionFromMenu(PathMenuLabel, 'author'); + await userEvent.click(container); + await selectOptionFromMenu(PredicateMenuLabel, 'Contains'); + const valueInput = await screen.getByPlaceholderText('type the value...'); + await userEvent.type(valueInput, 'iggy'); + await expectRowCountToBe(2); + + await userEvent.clear(valueInput); + + await userEvent.type(valueInput, 'goldilocks'); + await expectRowCountToBe(0); + }); + + it('shows all resources when the user has not typed anything in the value filter', async () => { + await expectRowCountToBe(10); + const mockResourcesForNextPage = [ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]; + + await getRowsForNextPage(mockResourcesForNextPage); + await expectRowCountToBe(3); + + await selectOptionFromMenu(PathMenuLabel, 'author'); + await userEvent.click(container); + await selectOptionFromMenu(PredicateMenuLabel, 'Contains'); + await expectRowCountToBe(3); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 6b02c418e..1ee2fce54 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -8,21 +8,37 @@ import { getResourceLabel } from '../../shared/utils'; import { DataExplorerTable } from './DataExplorerTable'; import './styles.less'; import { ProjectSelector } from './ProjectSelector'; -import { PredicateSelector, isPathMissing } from './PredicateSelector'; +import { + CONTAINS, + EMPTY_VALUE, + PredicateFilterT, + PredicateSelector, + doesResourceContain, + isPathMissing, +} from './PredicateSelector'; +import { normalizeString } from 'utils/stringUtils'; export interface DataExplorerConfiguration { pageSize: number; offset: number; orgAndProject?: [string, string]; predicatePath: string | null; - predicateFilter: string | null; + predicateFilter: PredicateFilterT | null; + predicateValue: string | null; } export const DataExplorer: React.FC<{}> = () => { const nexus = useNexusContext(); const [ - { pageSize, offset, orgAndProject, predicatePath, predicateFilter }, + { + pageSize, + offset, + orgAndProject, + predicatePath, + predicateFilter, + predicateValue, + }, updateTableConfiguration, ] = useReducer( ( @@ -35,6 +51,7 @@ export const DataExplorer: React.FC<{}> = () => { orgAndProject: undefined, predicatePath: null, predicateFilter: null, + predicateValue: '', } ); @@ -68,9 +85,20 @@ export const DataExplorer: React.FC<{}> = () => { const displayedDataSource = predicatePath && predicateFilter - ? currentPageDataSource.filter(resource => - isPathMissing(resource, predicatePath) - ) + ? currentPageDataSource.filter(resource => { + switch (predicateFilter) { + case EMPTY_VALUE: + return isPathMissing(resource, predicatePath); + case CONTAINS: + return doesResourceContain( + resource, + predicatePath, + predicateValue ?? '' + ); + default: + return true; + } + }) : currentPageDataSource; return ( diff --git a/src/subapps/dataExplorer/PredicateSelector.tsx b/src/subapps/dataExplorer/PredicateSelector.tsx index 209e0f53a..5e3d741bb 100644 --- a/src/subapps/dataExplorer/PredicateSelector.tsx +++ b/src/subapps/dataExplorer/PredicateSelector.tsx @@ -1,8 +1,10 @@ -import { Select } from 'antd'; -import React from 'react'; +import { Input, Select } from 'antd'; +import React, { useState } from 'react'; import { DataExplorerConfiguration, isObject } from './DataExplorer'; import './styles.less'; import { Resource } from '@bbp/nexus-sdk'; +import { normalizeString } from '../../utils/stringUtils'; +import { clsx } from 'clsx'; interface Props { dataSource: Resource[]; @@ -13,13 +15,18 @@ export const PredicateSelector: React.FC = ({ dataSource, onPredicateChange, }: Props) => { + const [selectedPredicateFilter, setSeletectPredicateFilter] = useState< + string + >(DEFAULT_OPTION); + const pathOptions = [ { value: DEFAULT_OPTION }, ...getAllPaths(dataSource).map(path => ({ value: path })), ]; - const predicateOptions = [ + const predicateFilterOptions: PredicateFilterOptions[] = [ { value: DEFAULT_OPTION }, - { value: 'Empty value' }, + { value: EMPTY_VALUE }, + { value: CONTAINS }, ]; return ( @@ -38,27 +45,59 @@ export const PredicateSelector: React.FC = ({ className="select-menu" popupClassName="search-menu" /> + = + { + onPredicateChange({ + predicateFilter: CONTAINS, + predicateValue: event.target.value, + }); + }} + /> + )}
); }; export const DEFAULT_OPTION = '-'; +export const EMPTY_VALUE = 'Empty value'; +export const CONTAINS = 'Contains'; +export type PredicateFilterT = typeof EMPTY_VALUE | typeof CONTAINS | null; + +type PredicateFilterOptions = { + value: Exclude | typeof DEFAULT_OPTION; +}; export const pathOptions = (paths: string[]) => [ { value: DEFAULT_OPTION }, @@ -107,7 +146,7 @@ export const isPathMissing = ( resource: { [key: string]: any }, path: string ): boolean => { - if (path in resource && path !== '') { + if (path in resource) { return false; } @@ -130,3 +169,38 @@ export const isPathMissing = ( } return true; }; + +export const doesResourceContain = ( + resource: { [key: string]: any }, + path: string, + value: string +): boolean => { + if (!Array.isArray(resource) && !isObject(resource)) { + return isSubstringOf(String(resource), value); + } + + const subpaths = path.split('.'); + + for (const subpath of subpaths) { + const valueAtSubpath = resource[subpath]; + const remainingPath = subpaths.slice(1); + if (Array.isArray(valueAtSubpath)) { + return valueAtSubpath.some(arrayElement => + doesResourceContain(arrayElement, remainingPath.join('.'), value) + ); + } + if (isObject(valueAtSubpath)) { + return doesResourceContain( + valueAtSubpath, + remainingPath.join('.'), + value + ); + } + return isSubstringOf(String(valueAtSubpath), value); + } + return isSubstringOf(String(resource), value); +}; + +const isSubstringOf = (text: string, maybeSubstring: string) => { + return normalizeString(text).includes(normalizeString(maybeSubstring)); +}; diff --git a/src/subapps/dataExplorer/predicate-filter.svg b/src/subapps/dataExplorer/predicate-filter.svg new file mode 100644 index 000000000..171a2c4ee --- /dev/null +++ b/src/subapps/dataExplorer/predicate-filter.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 69ce8ca8b..dcd571ec7 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -45,10 +45,13 @@ border: unset; } } + .select-menu { .ant-select-selector { color: @fusion-blue-8 !important; background-color: transparent !important; + max-width: max-content; + min-width: 200px; .ant-select-selection-item { font-weight: 700; @@ -61,6 +64,43 @@ } } } + + .select-menu.greyed-out { + .ant-select-selector { + color: @fusion-neutral-6 !important; + background-color: transparent !important; + text-transform: lowercase; + border: none; + max-width: max-content; + min-width: unset; + display: flex; + align-items: center; + } + + .ant-select-selector::before { + content: url('./predicate-filter.svg'); + width: 10px; + margin-right: 4px; + } + + .ant-select-arrow { + display: none; + } + + .ant-select-selection-item { + font-weight: 700; + padding-right: 0; + width: max-content; + } + } + + .predicate-value-input { + border-bottom: 1px solid @medium-gray; + color: @fusion-blue-8; + &::placeholder { + color: @fusion-blue-8; + } + } } .data-explorer-table { diff --git a/src/utils/stringUtils.ts b/src/utils/stringUtils.ts new file mode 100644 index 000000000..8e50df14a --- /dev/null +++ b/src/utils/stringUtils.ts @@ -0,0 +1 @@ +export const normalizeString = (str: string) => str.trim().toLowerCase(); From f61e4c6c11d1b80c1faf22f5f2343f615bdc9849 Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Mon, 3 Jul 2023 19:11:05 +0200 Subject: [PATCH 075/192] US 5 // Allow user to filter resources if they have path Signed-off-by: Dinika Saxena --- .../dataExplorer/DataExplorer-utils.spec.tsx | 255 ++++++++++++++++-- .../dataExplorer/DataExplorer.spec.tsx | 35 ++- src/subapps/dataExplorer/DataExplorer.tsx | 48 +--- .../dataExplorer/PredicateSelector.tsx | 132 ++++++--- 4 files changed, 366 insertions(+), 104 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx index d13a57358..d169c8d6a 100644 --- a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx @@ -1,7 +1,7 @@ import { doesResourceContain, getAllPaths, - isPathMissing, + checkPathExistence, } from './PredicateSelector'; describe('DataExplorerSpec-Utils', () => { @@ -82,7 +82,7 @@ describe('DataExplorerSpec-Utils', () => { expect(receivedPaths).toEqual(expectedPaths); }); - it('returns true when top level property does not exist in resource', () => { + it('checks if path exists in resource', () => { const resource = { foo: 'some value', nullValue: null, @@ -121,45 +121,229 @@ describe('DataExplorerSpec-Utils', () => { }, ], }; - expect(isPathMissing(resource, 'bar')).toEqual(true); - expect(isPathMissing(resource, 'nullValue')).toEqual(false); - expect(isPathMissing(resource, 'undefinedValue')).toEqual(false); - expect(isPathMissing(resource, 'emptyString')).toEqual(false); - expect(isPathMissing(resource, 'emptyArray')).toEqual(false); - expect(isPathMissing(resource, 'emptyObject')).toEqual(false); + expect(checkPathExistence(resource, 'bar')).toEqual(false); + expect(checkPathExistence(resource, 'nullValue')).toEqual(true); + expect(checkPathExistence(resource, 'undefinedValue')).toEqual(true); + expect(checkPathExistence(resource, 'emptyString')).toEqual(true); + expect(checkPathExistence(resource, 'emptyArray')).toEqual(true); + expect(checkPathExistence(resource, 'emptyObject')).toEqual(true); - expect(isPathMissing(resource, 'foo')).toEqual(false); - expect(isPathMissing(resource, 'foo.xyz')).toEqual(true); - expect(isPathMissing(resource, 'foo.distribution')).toEqual(true); + expect(checkPathExistence(resource, 'foo')).toEqual(true); + expect(checkPathExistence(resource, 'foo.xyz')).toEqual(false); + expect(checkPathExistence(resource, 'foo.distribution')).toEqual(false); - expect(isPathMissing(resource, 'distribution')).toEqual(false); - expect(isPathMissing(resource, 'distribution.name')).toEqual(false); - expect(isPathMissing(resource, 'distribution.name.sillyname')).toEqual( - true + expect(checkPathExistence(resource, 'distribution')).toEqual(true); + expect(checkPathExistence(resource, 'distribution.name')).toEqual(true); + expect(checkPathExistence(resource, 'distribution.name.sillyname')).toEqual( + false ); expect( - isPathMissing(resource, 'distribution.name.sillyname.pancake') + checkPathExistence(resource, 'distribution.name.sillyname.pancake') + ).toEqual(false); + expect( + checkPathExistence(resource, 'distribution.name.label.pancake') + ).toEqual(false); + expect( + checkPathExistence(resource, 'distribution.label.unofficial') + ).toEqual(true); // TODO: Add opposite + expect( + checkPathExistence(resource, 'distribution.label.extended.prefix') ).toEqual(true); - expect(isPathMissing(resource, 'distribution.name.label.pancake')).toEqual( - true + expect( + checkPathExistence(resource, 'distribution.label.extended.suffix') + ).toEqual(true); // Add opposite + expect( + checkPathExistence(resource, 'distribution.label.extended.notexisting') + ).toEqual(false); // Add opposite + expect(checkPathExistence(resource, 'distribution.foo')).toEqual(false); + expect(checkPathExistence(resource, 'distribution.emptyArray')).toEqual( + false ); - expect(isPathMissing(resource, 'distribution.label.unofficial')).toEqual( + expect( + checkPathExistence(resource, 'distribution.label.emptyArray') + ).toEqual(true); + expect( + checkPathExistence(resource, 'distribution.label.emptyString') + ).toEqual(true); // Add opposite + }); + + it('check if path exists in resource with nested array', () => { + const resource = { + distribution: [ + { + foo: 'foovalue', + filename: ['filename1'], + }, + { + foo: 'foovalue', + }, + ], + objPath: { + filename: ['filename1'], + }, + }; + expect( + checkPathExistence(resource, 'distribution.filename', 'exists') + ).toEqual(true); + expect( + checkPathExistence(resource, 'distribution.filename', 'does-not-exist') + ).toEqual(true); + expect( + checkPathExistence(resource, 'objPath.filename', 'does-not-exist') + ).toEqual(false); + expect(checkPathExistence(resource, 'objPath.filename', 'exists')).toEqual( true ); + }); + + it('checks if path is missing in resource', () => { + const resource = { + foo: 'some value', + nullValue: null, + undefinedValue: undefined, + emptyString: '', + emptyArray: [], + emptyObject: {}, + distribution: [ + { + name: 'sally', + label: { + official: 'official', + unofficial: 'unofficial', + emptyArray: [], + emptyString: '', + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + }, + { + name: 'sally', + sillyname: 'soliloquy', + label: [ + { + official: 'official', + emptyArray: [], + emptyString: '', + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + { + official: 'official', + unofficial: 'unofficial', + emptyArray: [1], + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + ], + }, + ], + }; + expect(checkPathExistence(resource, 'bar', 'does-not-exist')).toEqual(true); + expect(checkPathExistence(resource, 'nullValue', 'does-not-exist')).toEqual( + false + ); expect( - isPathMissing(resource, 'distribution.label.extended.prefix') + checkPathExistence(resource, 'undefinedValue', 'does-not-exist') ).toEqual(false); expect( - isPathMissing(resource, 'distribution.label.extended.suffix') - ).toEqual(true); - expect(isPathMissing(resource, 'distribution.foo')).toEqual(true); - expect(isPathMissing(resource, 'distribution.emptyArray')).toEqual(true); - expect(isPathMissing(resource, 'distribution.label.emptyArray')).toEqual( + checkPathExistence(resource, 'emptyString', 'does-not-exist') + ).toEqual(false); + expect( + checkPathExistence(resource, 'emptyArray', 'does-not-exist') + ).toEqual(false); + expect( + checkPathExistence(resource, 'emptyObject', 'does-not-exist') + ).toEqual(false); + + expect(checkPathExistence(resource, 'foo', 'does-not-exist')).toEqual( false ); - expect(isPathMissing(resource, 'distribution.label.emptyString')).toEqual( + expect(checkPathExistence(resource, 'foo.xyz', 'does-not-exist')).toEqual( true ); + expect( + checkPathExistence(resource, 'foo.distribution', 'does-not-exist') + ).toEqual(true); + + expect( + checkPathExistence(resource, 'distribution', 'does-not-exist') + ).toEqual(false); + expect( + checkPathExistence(resource, 'distribution.name', 'does-not-exist') + ).toEqual(false); + expect( + checkPathExistence( + resource, + 'distribution.name.sillyname', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.name.sillyname.pancake', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.name.label.pancake', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.label.unofficial', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.label.official', + 'does-not-exist' + ) + ).toEqual(false); + expect( + checkPathExistence( + resource, + 'distribution.label.extended.prefix', + 'does-not-exist' + ) + ).toEqual(false); + expect( + checkPathExistence( + resource, + 'distribution.label.extended.suffix', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.label.extended.notexisting', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence(resource, 'distribution.foo', 'does-not-exist') + ).toEqual(true); + expect( + checkPathExistence(resource, 'distribution.emptyArray', 'does-not-exist') + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.label.emptyArray', + 'does-not-exist' + ) + ).toEqual(false); + expect( + checkPathExistence( + resource, + 'distribution.label.emptyString', + 'does-not-exist' + ) + ).toEqual(true); }); it('checks if array strings can be checked for contains', () => { @@ -301,4 +485,23 @@ describe('DataExplorerSpec-Utils', () => { doesResourceContain(resource, 'distribution.filename', 'lly') ).toEqual(true); }); + + it('checks if path exists in resource', () => { + const resource = { + distribution: [ + { + name: 'sally', + filename: 'billy', + label: ['ChiPmunK'], + }, + { + name: 'sally', + sillyname: 'soliloquy', + filename: 'bolly', + label: { foo: 'foovalut', bar: 'barvalue' }, + }, + ], + }; + expect(checkPathExistence(resource, 'topLevelNotExisting')).toEqual(false); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 501a8a662..4456c1166 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -18,7 +18,13 @@ import { render, screen, waitFor } from '../../utils/testUtil'; import { DataExplorer } from './DataExplorer'; import { AllProjects } from './ProjectSelector'; import { getColumnTitle } from './DataExplorerTable'; -import { DEFAULT_OPTION, getAllPaths } from './PredicateSelector'; +import { + CONTAINS, + DEFAULT_OPTION, + DOES_NOT_EXIST, + EXISTS, + getAllPaths, +} from './PredicateSelector'; describe('DataExplorer', () => { const server = setupServer( @@ -73,7 +79,6 @@ describe('DataExplorer', () => { const expectRowCountToBe = async (expectedRowsCount: number) => { return await waitFor(() => { const rows = visibleTableRows(); - rows.forEach(row => console.log('Inner html', row.innerHTML)); expect(rows.length).toEqual(expectedRowsCount); return rows; }); @@ -176,7 +181,6 @@ describe('DataExplorer', () => { optionLabel: string ) => { await openMenuFor(menuAriaLabel); - console.log('Lookig for option ', optionLabel, ' in menu ', menuAriaLabel); const option = await getDropdownOption(optionLabel); await userEvent.click(option, { pointerEventsCheck: 0 }); }; @@ -398,11 +402,11 @@ describe('DataExplorer', () => { await expectRowCountToBe(3); await selectOptionFromMenu(PathMenuLabel, 'author'); - await selectOptionFromMenu(PredicateMenuLabel, 'Empty value'); + await selectOptionFromMenu(PredicateMenuLabel, DOES_NOT_EXIST); await expectRowCountToBe(1); await selectOptionFromMenu(PathMenuLabel, 'edition'); - await selectOptionFromMenu(PredicateMenuLabel, 'Empty value'); + await selectOptionFromMenu(PredicateMenuLabel, DOES_NOT_EXIST); await expectRowCountToBe(2); }); @@ -419,7 +423,7 @@ describe('DataExplorer', () => { await selectOptionFromMenu(PathMenuLabel, 'author'); await userEvent.click(container); - await selectOptionFromMenu(PredicateMenuLabel, 'Contains'); + await selectOptionFromMenu(PredicateMenuLabel, CONTAINS); const valueInput = await screen.getByPlaceholderText('type the value...'); await userEvent.type(valueInput, 'iggy'); await expectRowCountToBe(2); @@ -443,7 +447,24 @@ describe('DataExplorer', () => { await selectOptionFromMenu(PathMenuLabel, 'author'); await userEvent.click(container); - await selectOptionFromMenu(PredicateMenuLabel, 'Contains'); + await selectOptionFromMenu(PredicateMenuLabel, CONTAINS); + await expectRowCountToBe(3); + }); + + it('shows resources that have a path when user selects exists predicate', async () => { + await expectRowCountToBe(10); + const mockResourcesForNextPage = [ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]; + + await getRowsForNextPage(mockResourcesForNextPage); await expectRowCountToBe(3); + + await selectOptionFromMenu(PathMenuLabel, 'author'); + await userEvent.click(container); + await selectOptionFromMenu(PredicateMenuLabel, EXISTS); + await expectRowCountToBe(2); }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 1ee2fce54..2280e1d64 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -4,41 +4,23 @@ import { notification } from 'antd'; import { isString } from 'lodash'; import React, { useReducer } from 'react'; import { useQuery } from 'react-query'; -import { getResourceLabel } from '../../shared/utils'; import { DataExplorerTable } from './DataExplorerTable'; import './styles.less'; import { ProjectSelector } from './ProjectSelector'; -import { - CONTAINS, - EMPTY_VALUE, - PredicateFilterT, - PredicateSelector, - doesResourceContain, - isPathMissing, -} from './PredicateSelector'; -import { normalizeString } from 'utils/stringUtils'; +import { PredicateSelector } from './PredicateSelector'; export interface DataExplorerConfiguration { pageSize: number; offset: number; orgAndProject?: [string, string]; - predicatePath: string | null; - predicateFilter: PredicateFilterT | null; - predicateValue: string | null; + predicateFilter: ((resource: Resource) => boolean) | null; } export const DataExplorer: React.FC<{}> = () => { const nexus = useNexusContext(); const [ - { - pageSize, - offset, - orgAndProject, - predicatePath, - predicateFilter, - predicateValue, - }, + { pageSize, offset, orgAndProject, predicateFilter }, updateTableConfiguration, ] = useReducer( ( @@ -49,9 +31,7 @@ export const DataExplorer: React.FC<{}> = () => { pageSize: 50, offset: 0, orgAndProject: undefined, - predicatePath: null, predicateFilter: null, - predicateValue: '', } ); @@ -83,23 +63,11 @@ export const DataExplorer: React.FC<{}> = () => { const currentPageDataSource: Resource[] = resources?._results || []; - const displayedDataSource = - predicatePath && predicateFilter - ? currentPageDataSource.filter(resource => { - switch (predicateFilter) { - case EMPTY_VALUE: - return isPathMissing(resource, predicatePath); - case CONTAINS: - return doesResourceContain( - resource, - predicatePath, - predicateValue ?? '' - ); - default: - return true; - } - }) - : currentPageDataSource; + const displayedDataSource = predicateFilter + ? currentPageDataSource.filter(resource => { + return predicateFilter(resource); + }) + : currentPageDataSource; return (
diff --git a/src/subapps/dataExplorer/PredicateSelector.tsx b/src/subapps/dataExplorer/PredicateSelector.tsx index 5e3d741bb..62d463675 100644 --- a/src/subapps/dataExplorer/PredicateSelector.tsx +++ b/src/subapps/dataExplorer/PredicateSelector.tsx @@ -15,9 +15,12 @@ export const PredicateSelector: React.FC = ({ dataSource, onPredicateChange, }: Props) => { - const [selectedPredicateFilter, setSeletectPredicateFilter] = useState< - string - >(DEFAULT_OPTION); + const [path, setPath] = useState(DEFAULT_OPTION); + + const [predicate, setPredicate] = useState( + DEFAULT_OPTION + ); + const [searchTerm, setSearchTerm] = useState(null); const pathOptions = [ { value: DEFAULT_OPTION }, @@ -25,10 +28,46 @@ export const PredicateSelector: React.FC = ({ ]; const predicateFilterOptions: PredicateFilterOptions[] = [ { value: DEFAULT_OPTION }, - { value: EMPTY_VALUE }, + { value: EXISTS }, + { value: DOES_NOT_EXIST }, { value: CONTAINS }, ]; + const predicateSelected = ( + path: string, + predicate: PredicateFilterOptions['value'], + searchTerm: string | null + ) => { + if (path === DEFAULT_OPTION || predicate === DEFAULT_OPTION) { + onPredicateChange({ predicateFilter: null }); + } + + switch (predicate) { + case EXISTS: + onPredicateChange({ + predicateFilter: (resource: Resource) => + checkPathExistence(resource, path, 'exists'), + }); + break; + case DOES_NOT_EXIST: + onPredicateChange({ + predicateFilter: (resource: Resource) => + checkPathExistence(resource, path, 'does-not-exist'), + }); + break; + case CONTAINS: + if (searchTerm) { + onPredicateChange({ + predicateFilter: (resource: Resource) => + doesResourceContain(resource, path, searchTerm), + }); + } + break; + default: + onPredicateChange({ predicateFilter: null }); + } + }; + return (
with @@ -36,9 +75,8 @@ export const PredicateSelector: React.FC = ({ { - setSeletectPredicateFilter(predicateFilterLabel); - if (predicateFilterLabel === CONTAINS) { - return; - } - onPredicateChange({ - predicateFilter: - predicateFilterLabel === DEFAULT_OPTION - ? null - : predicateFilterLabel, - }); + onSelect={(predicateLabel: PredicateFilterOptions['value']) => { + setPredicate(predicateLabel); + predicateSelected(path, predicateLabel, searchTerm); }} aria-label="predicate-selector" - className={clsx( - 'select-menu', - selectedPredicateFilter === CONTAINS && 'greyed-out' - )} + className={clsx('select-menu', path === CONTAINS && 'greyed-out')} popupClassName="search-menu" allowClear={true} - onClear={() => onPredicateChange({ predicateFilter: null })} + onClear={() => { + setPredicate(DEFAULT_OPTION); + predicateSelected(path, DEFAULT_OPTION, searchTerm); + }} /> - {selectedPredicateFilter === CONTAINS && ( + {predicate === CONTAINS && ( { - onPredicateChange({ - predicateFilter: CONTAINS, - predicateValue: event.target.value, - }); + setSearchTerm(event.target.value); + predicateSelected(path, predicate, event.target.value); }} /> )} @@ -91,9 +119,15 @@ export const PredicateSelector: React.FC = ({ }; export const DEFAULT_OPTION = '-'; -export const EMPTY_VALUE = 'Empty value'; +export const DOES_NOT_EXIST = 'Does not exist'; +export const EXISTS = 'Exists'; export const CONTAINS = 'Contains'; -export type PredicateFilterT = typeof EMPTY_VALUE | typeof CONTAINS | null; + +export type PredicateFilterT = + | typeof DOES_NOT_EXIST + | typeof EXISTS + | typeof CONTAINS + | null; type PredicateFilterOptions = { value: Exclude | typeof DEFAULT_OPTION; @@ -142,11 +176,47 @@ const getPathsForResource = ( return paths; }; +export const checkPathExistence = ( + resource: { [key: string]: any }, + path: string, + criteria: 'exists' | 'does-not-exist' = 'exists' +): boolean => { + if (path in resource) { + return criteria === 'exists' ? true : false; + } + + const subpaths = path.split('.'); + + for (const subpath of subpaths) { + const valueAtSubpath = resource[subpath]; + const remainingPath = subpaths.slice(1); + if (!(subpath in resource)) { + return criteria === 'exists' ? false : true; + } + + if (Array.isArray(valueAtSubpath)) { + return valueAtSubpath.some(value => + checkPathExistence(value, remainingPath.join('.'), criteria) + ); + } + if (isObject(valueAtSubpath)) { + return checkPathExistence( + valueAtSubpath, + remainingPath.join('.'), + criteria + ); + } + break; + } + + return criteria === 'exists' ? false : true; +}; + export const isPathMissing = ( resource: { [key: string]: any }, path: string ): boolean => { - if (path in resource) { + if (path in resource && path !== '') { return false; } From 6c6be4781c4aa39e0d5b9149a932f5d7baa87be6 Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Tue, 4 Jul 2023 16:17:32 +0200 Subject: [PATCH 076/192] US 7 // Filter resources that do not contain value Signed-off-by: Dinika Saxena --- .../dataExplorer/DataExplorer-utils.spec.tsx | 123 ++++++++++++++++++ .../dataExplorer/DataExplorer.spec.tsx | 32 +++++ .../dataExplorer/PredicateSelector.tsx | 99 ++++++++------ 3 files changed, 212 insertions(+), 42 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx index d169c8d6a..747f423c8 100644 --- a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx @@ -504,4 +504,127 @@ describe('DataExplorerSpec-Utils', () => { }; expect(checkPathExistence(resource, 'topLevelNotExisting')).toEqual(false); }); + + it('checks if resource does not contain value in path', () => { + const resource = { + distribution: [ + { + name: 'sally', + filename: 'billy', + label: ['ChiPmunK'], + }, + { + name: 'sally', + sillyname: 'soliloquy', + filename: 'bolly', + label: { foo: 'foovalut', bar: 'barvalue' }, + }, + ], + }; + + expect( + doesResourceContain(resource, 'distribution', 'sally', 'does-not-contain') + ).toEqual(true); + + expect( + doesResourceContain( + resource, + 'distribution.name', + 'sally', + 'does-not-contain' + ) + ).toEqual(false); + + expect( + doesResourceContain( + resource, + 'distribution.filename', + 'billy', + 'does-not-contain' + ) + ).toEqual(true); + + expect( + doesResourceContain( + resource, + 'distribution.filename', + 'popeye', + 'does-not-contain' + ) + ).toEqual(true); + }); + + it('checks if resource does not contain value for nested paths', () => { + const resource = { + distribution: [ + { + name: 'sally', + filename: 'billy', + label: ['ChiPmunK'], + nested: [{ prop1: 'value1', prop2: ['value2', 'value3'] }], + }, + { + name: 'sally', + sillyname: 'soliloquy', + filename: 'bolly', + label: { foo: 'foovalut', bar: 'barvalue' }, + nested: [{ prop1: 'value1', prop2: ['value2', 'value5'] }], + }, + ], + }; + + expect( + doesResourceContain( + resource, + 'distribution.label', + 'chipmunk', + 'does-not-contain' + ) + ).toEqual(true); + + expect( + doesResourceContain( + resource, + 'distribution.label', + 'crazy', + 'does-not-contain' + ) + ).toEqual(true); + + expect( + doesResourceContain( + resource, + 'distribution.nested', + 'crazy', + 'does-not-contain' + ) + ).toEqual(true); + + expect( + doesResourceContain( + resource, + 'distribution.nested.prop2', + 'value2', + 'does-not-contain' + ) + ).toEqual(true); + + expect( + doesResourceContain( + resource, + 'distribution.nested.prop2', + 'value', + 'does-not-contain' + ) + ).toEqual(false); + + expect( + doesResourceContain( + resource, + 'distribution.nested.prop2', + 'value5', + 'does-not-contain' + ) + ).toEqual(true); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 4456c1166..5de9fd30b 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -21,6 +21,7 @@ import { getColumnTitle } from './DataExplorerTable'; import { CONTAINS, DEFAULT_OPTION, + DOES_NOT_CONTAIN, DOES_NOT_EXIST, EXISTS, getAllPaths, @@ -467,4 +468,35 @@ describe('DataExplorer', () => { await selectOptionFromMenu(PredicateMenuLabel, EXISTS); await expectRowCountToBe(2); }); + + it('filters by resources that do not contain value provided by user', async () => { + await expectRowCountToBe(10); + const mockResourcesForNextPage = [ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]; + + await getRowsForNextPage(mockResourcesForNextPage); + await expectRowCountToBe(3); + + await selectOptionFromMenu(PathMenuLabel, 'author'); + await userEvent.click(container); + await selectOptionFromMenu(PredicateMenuLabel, DOES_NOT_CONTAIN); + const valueInput = await screen.getByPlaceholderText('type the value...'); + await userEvent.type(valueInput, 'iggy'); + await expectRowCountToBe(2); + + await userEvent.clear(valueInput); + await userEvent.type(valueInput, 'goldilocks'); + await expectRowCountToBe(3); + + await userEvent.clear(valueInput); + await userEvent.type(valueInput, 'piggy'); + await expectRowCountToBe(2); + + await userEvent.clear(valueInput); + await userEvent.type(valueInput, 'arch'); + await expectRowCountToBe(3); + }); }); diff --git a/src/subapps/dataExplorer/PredicateSelector.tsx b/src/subapps/dataExplorer/PredicateSelector.tsx index 62d463675..2782fd492 100644 --- a/src/subapps/dataExplorer/PredicateSelector.tsx +++ b/src/subapps/dataExplorer/PredicateSelector.tsx @@ -31,6 +31,7 @@ export const PredicateSelector: React.FC = ({ { value: EXISTS }, { value: DOES_NOT_EXIST }, { value: CONTAINS }, + { value: DOES_NOT_CONTAIN }, ]; const predicateSelected = ( @@ -59,7 +60,20 @@ export const PredicateSelector: React.FC = ({ if (searchTerm) { onPredicateChange({ predicateFilter: (resource: Resource) => - doesResourceContain(resource, path, searchTerm), + doesResourceContain(resource, path, searchTerm, 'contains'), + }); + } + break; + case DOES_NOT_CONTAIN: + if (searchTerm) { + onPredicateChange({ + predicateFilter: (resource: Resource) => + doesResourceContain( + resource, + path, + searchTerm, + 'does-not-contain' + ), }); } break; @@ -68,6 +82,9 @@ export const PredicateSelector: React.FC = ({ } }; + const shouldShowValueInput = + predicate === CONTAINS || predicate === DOES_NOT_CONTAIN; + return (
with @@ -93,7 +110,7 @@ export const PredicateSelector: React.FC = ({ predicateSelected(path, predicateLabel, searchTerm); }} aria-label="predicate-selector" - className={clsx('select-menu', path === CONTAINS && 'greyed-out')} + className={clsx('select-menu', shouldShowValueInput && 'greyed-out')} popupClassName="search-menu" allowClear={true} onClear={() => { @@ -102,12 +119,13 @@ export const PredicateSelector: React.FC = ({ }} /> - {predicate === CONTAINS && ( + {shouldShowValueInput && ( { setSearchTerm(event.target.value); predicateSelected(path, predicate, event.target.value); @@ -122,11 +140,13 @@ export const DEFAULT_OPTION = '-'; export const DOES_NOT_EXIST = 'Does not exist'; export const EXISTS = 'Exists'; export const CONTAINS = 'Contains'; +export const DOES_NOT_CONTAIN = 'Does not contain'; export type PredicateFilterT = | typeof DOES_NOT_EXIST | typeof EXISTS | typeof CONTAINS + | typeof DOES_NOT_CONTAIN | null; type PredicateFilterOptions = { @@ -212,41 +232,14 @@ export const checkPathExistence = ( return criteria === 'exists' ? false : true; }; -export const isPathMissing = ( - resource: { [key: string]: any }, - path: string -): boolean => { - if (path in resource && path !== '') { - return false; - } - - const subpaths = path.split('.'); - - for (const subpath of subpaths) { - if (!(subpath in resource)) { - return true; - } - const valueAtSubpath = resource[subpath]; - const remainingPath = subpaths.slice(1); - if (Array.isArray(valueAtSubpath)) { - return valueAtSubpath.some(value => - isPathMissing(value, remainingPath.join('.')) - ); - } - if (isObject(valueAtSubpath)) { - return isPathMissing(valueAtSubpath, remainingPath.join('.')); - } - } - return true; -}; - export const doesResourceContain = ( resource: { [key: string]: any }, path: string, - value: string + value: string, + criteria: 'contains' | 'does-not-contain' = 'contains' ): boolean => { - if (!Array.isArray(resource) && !isObject(resource)) { - return isSubstringOf(String(resource), value); + if (isPrimitiveValue(resource)) { + return isSubstringOf(String(resource), value, criteria === 'contains'); } const subpaths = path.split('.'); @@ -255,22 +248,44 @@ export const doesResourceContain = ( const valueAtSubpath = resource[subpath]; const remainingPath = subpaths.slice(1); if (Array.isArray(valueAtSubpath)) { - return valueAtSubpath.some(arrayElement => - doesResourceContain(arrayElement, remainingPath.join('.'), value) - ); + return valueAtSubpath.some(arrayElement => { + return doesResourceContain( + arrayElement, + remainingPath.join('.'), + value, + criteria + ); + }); } if (isObject(valueAtSubpath)) { return doesResourceContain( valueAtSubpath, remainingPath.join('.'), - value + value, + criteria ); } - return isSubstringOf(String(valueAtSubpath), value); + return isSubstringOf( + String(valueAtSubpath), + value, + criteria === 'contains' + ); + } + return isSubstringOf(String(resource), value, criteria === 'contains'); +}; + +const isSubstringOf = ( + text: string, + maybeSubstring: string, + shouldContain: boolean +) => { + if (shouldContain) { + return normalizeString(text).includes(normalizeString(maybeSubstring)); } - return isSubstringOf(String(resource), value); + return !normalizeString(text).includes(normalizeString(maybeSubstring)); }; -const isSubstringOf = (text: string, maybeSubstring: string) => { - return normalizeString(text).includes(normalizeString(maybeSubstring)); +// Returns true if value is not an array, object, or function. +const isPrimitiveValue = (value: any) => { + return !Array.isArray(value) && !isObject(value); }; From 075f897f555fe6e32fd10100544bae20d5ea020d Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Tue, 4 Jul 2023 18:15:23 +0200 Subject: [PATCH 077/192] US 8 // Navigate to resource view to see details Signed-off-by: Dinika Saxena --- .../handlers/DataExplorer/handlers.ts | 2 +- .../components/ResourceEditor/CodeEditor.tsx | 2 +- .../ResourceEditor/ResourceEditor.spec.tsx | 2 +- .../components/ResourceEditor/index.tsx | 2 +- .../dataExplorer/DataExplorer-utils.spec.tsx | 2 +- .../dataExplorer/DataExplorer.spec.tsx | 48 ++++++++++++++++--- .../dataExplorer/DataExplorerTable.tsx | 20 +++++++- .../dataExplorer/PredicateSelector.tsx | 5 ++ src/subapps/dataExplorer/styles.less | 2 +- 9 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/__mocks__/handlers/DataExplorer/handlers.ts b/src/__mocks__/handlers/DataExplorer/handlers.ts index 00018b805..dfe28a432 100644 --- a/src/__mocks__/handlers/DataExplorer/handlers.ts +++ b/src/__mocks__/handlers/DataExplorer/handlers.ts @@ -130,5 +130,5 @@ export const defaultMockResult: Resource[] = [ getMockResource('self7', { specialProperty: { foo: 1, bar: 2 } }, 'unhcr'), getMockResource('self8', { specialProperty: null }), getMockResource('self9', { specialProperty: {} }), - getMockResource('self10', {}), + getMockResource('self10', { specialProperty: undefined }), ]; diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index f6b656cd6..5c893cb04 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -38,7 +38,7 @@ const CodeEditor = forwardRef( return ( { const onLinksFound = jest.fn(); const { queryByText, container } = render( {}} diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 4784b34a9..2330b2c5a 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -194,7 +194,7 @@ const ResourceEditor: React.FunctionComponent = props => { return (
{showControlPanel && ( diff --git a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx index 747f423c8..4db660ff9 100644 --- a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx @@ -607,7 +607,7 @@ describe('DataExplorerSpec-Utils', () => { 'value2', 'does-not-contain' ) - ).toEqual(true); + ).toEqual(true); // This is expected since the in the arrays ([`value2`, `value3`] & [`value2`, `value5`]) there is atleast 1 element (`value3` in the 1st array and value5 in the 2nd) that does not contain "value2" expect( doesResourceContain( diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 5de9fd30b..48fd5b51b 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -26,6 +26,10 @@ import { EXISTS, getAllPaths, } from './PredicateSelector'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import configureStore from '../../shared/store'; describe('DataExplorer', () => { const server = setupServer( @@ -33,6 +37,7 @@ describe('DataExplorer', () => { filterByProjectHandler(defaultMockResult), getProjectHandler() ); + const history = createMemoryHistory({}); let container: HTMLElement; let user: UserEvent; @@ -46,13 +51,18 @@ describe('DataExplorer', () => { fetch, uri: deltaPath(), }); + const store = configureStore(history, { nexus }, {}); dataExplorerPage = ( - - - - - + + + + + + + + + ); component = render(dataExplorerPage); @@ -112,6 +122,18 @@ describe('DataExplorer', () => { return allCellsForRow[colIndex].textContent; }; + const getRowForResource = async (resource: Resource) => { + const selfCell = await screen.getAllByText( + new RegExp(resource._self, 'i'), + { + selector: 'td', + } + ); + const row = selfCell[0].parentElement; + expect(row).toBeInTheDocument(); + return row!; + }; + const openProjectAutocomplete = async () => { const projectAutocomplete = await getProjectAutocomplete(); await userEvent.click(projectAutocomplete); @@ -293,7 +315,7 @@ describe('DataExplorer', () => { expect(textForSpecialProperty).toMatch(/No data/i); }); - it('shows No data text when values is null', async () => { + it('does not show No data text when values is null', async () => { await expectRowCountToBe(10); const resourceWithUndefinedProperty = defaultMockResult.find( res => res.specialProperty === null @@ -302,7 +324,8 @@ describe('DataExplorer', () => { resourceWithUndefinedProperty, 'specialProperty' ); - expect(textForSpecialProperty).toMatch(/No data/i); + expect(textForSpecialProperty).not.toMatch(/No data/i); + expect(textForSpecialProperty).toMatch(/null/); }); it('does not show No data when value is empty string', async () => { @@ -499,4 +522,15 @@ describe('DataExplorer', () => { await userEvent.type(valueInput, 'arch'); await expectRowCountToBe(3); }); + + it('navigates to resource view when user clicks on row', async () => { + await expectRowCountToBe(10); + + expect(history.location.pathname).not.toContain('self1'); + + const firstDataRow = await getRowForResource(defaultMockResult[0]); + await userEvent.click(firstDataRow); + + expect(history.location.pathname).toContain('self1'); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index 0f4a4b9c0..c27dfce2f 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -8,6 +8,8 @@ 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'; interface TDataExplorerTable { isLoading: boolean; @@ -30,6 +32,9 @@ export const DataExplorerTable: React.FC = ({ offset, updateTableConfiguration, }: TDataExplorerTable) => { + const history = useHistory(); + const location = useLocation(); + const allowedTotal = total ? (total > 10000 ? 10000 : total) : undefined; const tablePaginationConfig: TablePaginationConfig = { @@ -48,11 +53,23 @@ export const DataExplorerTable: React.FC = ({ 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)} dataSource={dataSource} rowKey={record => record._self} + onRow={resource => ({ + onClick: _ => goToResource(resource), + })} loading={isLoading} bordered={false} className="data-explorer-table" @@ -98,7 +115,8 @@ const defaultColumnConfig = (colName: string): ColumnType => { className: `data-explorer-column data-explorer-column-${colName}`, sorter: false, render: text => { - if (text === undefined || text === null) { + if (text === undefined) { + // Text will also be undefined if a certain resource does not have `colName` as its property return ; } return <>{JSON.stringify(text)}; diff --git a/src/subapps/dataExplorer/PredicateSelector.tsx b/src/subapps/dataExplorer/PredicateSelector.tsx index 2782fd492..3622b672d 100644 --- a/src/subapps/dataExplorer/PredicateSelector.tsx +++ b/src/subapps/dataExplorer/PredicateSelector.tsx @@ -232,6 +232,11 @@ export const checkPathExistence = ( return criteria === 'exists' ? false : true; }; +/** + * Returns true if `path` in resource matches the crtieria (ie contains or does not contain) for the given value. + * + * If resource is an array, then the return value is true if any one element in that array matches the criteria. + */ export const doesResourceContain = ( resource: { [key: string]: any }, path: string, diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index dcd571ec7..7edd3c027 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -5,7 +5,7 @@ padding-left: 40px; background: @fusion-main-bg; width: 100%; - margin-top: 30px; + margin-top: 60px; .data-explorer-header { display: flex; From 7ed536212646d92a7f51f12c0f25cfaf158138a9 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 5 Jul 2023 13:12:03 +0200 Subject: [PATCH 078/192] update: add referer to data explorer to allow return back to origin of graph flow --- .../canvas/DataExplorerGraphFlow/styles.less | 1 - src/shared/containers/ResourceEditor.tsx | 2 ++ .../NavigationBackButton.tsx | 31 ++++++++++++++++--- .../styles.less | 3 ++ .../ResolvedLinkEditorPopover.tsx | 6 ++-- src/shared/store/reducers/data-explorer.ts | 9 +++++- 6 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/styles.less b/src/shared/canvas/DataExplorerGraphFlow/styles.less index c1543b8b2..f2a5b0a41 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/styles.less +++ b/src/shared/canvas/DataExplorerGraphFlow/styles.less @@ -37,7 +37,6 @@ align-items: flex-start; justify-content: space-between; gap: 10px; - margin-top: 52px; } &.no-current { diff --git a/src/shared/containers/ResourceEditor.tsx b/src/shared/containers/ResourceEditor.tsx index e1b9501bf..bcce0ee0f 100644 --- a/src/shared/containers/ResourceEditor.tsx +++ b/src/shared/containers/ResourceEditor.tsx @@ -19,6 +19,7 @@ import { InitNewVisitDataExplorerGraphView, } from '../store/reducers/data-explorer'; import { getOrgAndProjectFromResourceObject, getResourceLabel } from '../utils'; +import { pick } from 'lodash'; const ResourceEditorContainer: React.FunctionComponent<{ resourceId: string; @@ -131,6 +132,7 @@ const ResourceEditorContainer: React.FunctionComponent<{ } else { dispatch( InitNewVisitDataExplorerGraphView({ + referer: pick(location, ['pathname', 'search', 'state']), current: { _self: data._self, types: getNormalizedTypes(data['@type']), diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx index 9d6a93c65..90487be8d 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx @@ -2,7 +2,12 @@ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory, useLocation } from 'react-router'; import { ArrowLeftOutlined } from '@ant-design/icons'; -import { ReturnBackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +import { clsx } from 'clsx'; +import { + DATA_EXPLORER_GRAPH_FLOW_DIGEST, + ResetDataExplorerGraphFlow, + ReturnBackDataExplorerGraphFlow, +} from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; import './styles.less'; @@ -10,13 +15,31 @@ const NavigationBack = () => { const dispatch = useDispatch(); const history = useHistory(); const location = useLocation(); - const { links } = useSelector((state: RootState) => state.dataExplorer); + const { links, referer } = useSelector( + (state: RootState) => state.dataExplorer + ); + const onBack = () => { + if (referer?.pathname && !links.length) { + dispatch(ResetDataExplorerGraphFlow({ initialState: null })); + localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + history.push(`${referer.pathname}${referer.search}`, { + ...referer.state, + }); + return; + } history.replace(location.pathname); dispatch(ReturnBackDataExplorerGraphFlow()); }; - return links.length ? ( - diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index e6429a520..0342f81fa 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -130,6 +130,9 @@ color: @fusion-daybreak-7; } } + &.go-back-to-referer { + margin-left: 40px; + } } .degf-content__haeder { diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx index 841bce1ab..d08453b9c 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx @@ -5,7 +5,8 @@ import { useHistory, useLocation, useRouteMatch } from 'react-router'; import { useNexusContext } from '@bbp/react-nexus'; import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { clsx } from 'clsx'; -import { Button, Tag } from 'antd'; +import { Tag } from 'antd'; +import { pick } from 'lodash'; import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; import { match as pmatch } from 'ts-pattern'; import { UISettingsActionTypes } from '../../store/actions/ui-settings'; @@ -90,7 +91,7 @@ const ResolvedLinkEditorPopover = () => { const navigate = useHistory(); const dispatch = useDispatch(); const nexus = useNexusContext(); - const { pathname } = useLocation(); + const { pathname, search, state } = useLocation(); const routeMatch = useRouteMatch<{ orgLabel: string; projectLabel: string; @@ -120,6 +121,7 @@ const ResolvedLinkEditorPopover = () => { const orgProject = getOrgAndProjectFromProjectId(data._project); dispatch( InitNewVisitDataExplorerGraphView({ + referer: { pathname, search, state }, source: { _self: data._self, title: getResourceLabel(data), diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 83c0bb90b..8beb9a507 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -21,6 +21,11 @@ export type TDELink = { export type TDataExplorerState = { links: TDELink[]; current: TDELink | null; + referer?: { + pathname: string; + search: string; + state: Record; + } | null; shrinked: boolean; limited: boolean; highlightIndex: number; @@ -29,6 +34,7 @@ export type TDataExplorerState = { const initialState: TDataExplorerState = { links: [], current: null, + referer: null, shrinked: false, limited: false, highlightIndex: -1, @@ -70,10 +76,11 @@ export const dataExplorerSlice = createSlice({ }, InitNewVisitDataExplorerGraphView: ( state, - { payload: { source, current, limited } } + { payload: { source, current, limited, referer } } ) => { const newState = { ...state, + referer, current, limited, links: From 5a205337ad381586bab4136afc19ece821f510c9 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 5 Jul 2023 13:32:16 +0200 Subject: [PATCH 079/192] update: allow anonymous users to see resource-view --- src/shared/App.tsx | 2 +- .../components/ResourceEditor/index.tsx | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/shared/App.tsx b/src/shared/App.tsx index aeaa1e657..0d569b958 100644 --- a/src/shared/App.tsx +++ b/src/shared/App.tsx @@ -55,9 +55,9 @@ const App: React.FC = () => { + {userAuthenticated && ( - diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 2330b2c5a..d0484b432 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -78,7 +78,14 @@ const ResourceEditor: React.FunctionComponent = props => { const [stringValue, setStringValue] = React.useState( JSON.stringify(rawData, null, 2) ); - const { limited } = useSelector((state: RootState) => state.dataExplorer); + const { + dataExplorer: { limited }, + oidc, + } = useSelector((state: RootState) => ({ + dataExplorer: state.dataExplorer, + oidc: state.oidc, + })); + const userAuthenticated = oidc.user && oidc.user.access_token; const dispatch = useDispatch(); const keyFoldCode = (cm: any) => { cm.foldCode(cm.getCursor()); @@ -247,16 +254,18 @@ const ResourceEditor: React.FunctionComponent = props => { style={switchMarginRight} /> )} - {' '} + {userAuthenticated && ( + + )}{' '} {editable && isEditing && ( {result.isDownloadable && ( -
- {downloadInProgress ? ( - - ) : ( - downloadBinaryAsync(result)} - /> - )} -
+ + downloadBinaryAsync({ + orgLabel: result.resource?.[0]!, + projectLabel: result.resource?.[1]!, + resourceId: result.resource?.[2]!, + ext: result.resource?.[4] ?? 'json', + title: result.title, + }) + } + /> )}
@@ -195,53 +136,37 @@ const ResolvedLinkEditorPopover = () => { }) .with({ open: true, resolvedAs: 'resources' }, () => { return ( - + {(results as TDELink[]).map(item => (
{item.resource?.[0] && item.resource?.[1] && ( {`${item.resource?.[0]}/${item.resource?.[1]}`} )} {item.isDownloadable && ( -
- {downloadInProgress ? ( - - ) : ( - downloadBinaryAsync(item)} - /> - )} -
+ + downloadBinaryAsync({ + orgLabel: item.resource?.[0]!, + projectLabel: item.resource?.[1]!, + resourceId: item.resource?.[2]!, + ext: item.resource?.[4] ?? 'json', + title: item.title, + }) + } + /> )}
))}
); }) - .with({ open: true, resolvedAs: 'external' }, () => { - const result = results as TDELink; - return ( - -
- External Link - - {result.title} - -
-
- ); - }) .otherwise(() => <>); }; From ef8eeb4bcb3268a3a12db72e9696ff74148147fa Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 6 Jul 2023 16:39:12 +0200 Subject: [PATCH 087/192] de-13/update: tests for popover --- .../handlers/ResourceEditor/handlers.ts | 1 + .../ResourceEditor/editorUtils.spec.tsx | 145 +++++++++--------- 2 files changed, 77 insertions(+), 69 deletions(-) diff --git a/src/__mocks__/handlers/ResourceEditor/handlers.ts b/src/__mocks__/handlers/ResourceEditor/handlers.ts index 3cf051d47..e2f3788be 100644 --- a/src/__mocks__/handlers/ResourceEditor/handlers.ts +++ b/src/__mocks__/handlers/ResourceEditor/handlers.ts @@ -483,6 +483,7 @@ const getSearchApiResponseObject = rest.get( export { resourceResolverApi, + resourceResolverApiId, resourceFromSearchApiId, resourceFromSearchApi, getResolverResponseObject, diff --git a/src/shared/components/ResourceEditor/editorUtils.spec.tsx b/src/shared/components/ResourceEditor/editorUtils.spec.tsx index 61731dcb1..94d51894e 100644 --- a/src/shared/components/ResourceEditor/editorUtils.spec.tsx +++ b/src/shared/components/ResourceEditor/editorUtils.spec.tsx @@ -1,6 +1,6 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { resolveLinkInEditor, getNormalizedTypes } from './editorUtils'; +import { getNormalizedTypes, editorLinkResolutionHandler } from './editorUtils'; import { Provider } from 'react-redux'; import { createMemoryHistory } from 'history'; import { NexusClient, createNexusClient } from '@bbp/nexus-sdk'; @@ -12,19 +12,37 @@ import { AnyAction, Store } from 'redux'; import { QueryClientProvider, QueryClient } from 'react-query'; import { setupServer } from 'msw/node'; import { - resourceResolverApi, resourceFromSearchApiId, resourceFromSearchApi, getResolverResponseObject, getSearchApiResponseObject, } from '../../../__mocks__/handlers/ResourceEditor/handlers'; +import { getResourceLabel } from '../../utils'; import { - getOrgAndProjectFromResourceObject, - getResourceLabel, -} from '../../utils'; -import { render, screen, waitFor, act, cleanup } from '../../../utils/testUtil'; + render, + screen, + waitFor, + cleanup, + RenderResult, +} from '../../../utils/testUtil'; import ResolvedLinkEditorPopover from '../../molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover'; +import { UISettingsActionTypes } from 'shared/store/actions/ui-settings'; +document.createRange = () => { + const range = new Range(); + + range.getBoundingClientRect = jest.fn(); + + range.getClientRects = () => { + return { + item: () => null, + length: 0, + [Symbol.iterator]: jest.fn(), + }; + }; + + return range; +}; describe('getNormalizedTypes', () => { const typesAsString = 'Resource'; it('should return the normalized types', () => { @@ -64,12 +82,15 @@ describe('resolveLinkInEditor', () => { const defaultPaylaod = { top: 0, left: 0, open: true }; let nexus: NexusClient; let TestApp: JSX.Element; + let component: RenderResult; + let rerender: (ui: React.ReactElement) => void; + beforeAll(() => { server = setupServer(getResolverResponseObject, getSearchApiResponseObject); server.listen(); }); - beforeAll(async () => { + beforeEach(async () => { const history = createMemoryHistory({}); nexus = createNexusClient({ fetch, @@ -87,12 +108,10 @@ describe('resolveLinkInEditor', () => { ); + component = render(TestApp); + rerender = component.rerender; }); - beforeEach(async () => { - await act(async () => { - await render(TestApp); - }); - }); + afterEach(async () => { cleanup(); }); @@ -100,76 +119,56 @@ describe('resolveLinkInEditor', () => { server.resetHandlers(); server.close(); }); - // case-0: the url is not valid + // case-resource: can not be tested since codemirror issue and redirection + // case-error: link can not be resolved by the project resolver nor the search api and it's not external it('should return null if the url is not valid', async () => { - const url = 'not a valid url'; - const result = await resolveLinkInEditor({ + const url = 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/invalid'; + const { resolvedAs, error, results } = await editorLinkResolutionHandler({ nexus, url, - defaultPaylaod, - dispatch: store.dispatch, orgLabel: 'orgLabel', projectLabel: 'projectLabel', }); + store.dispatch({ + type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, + payload: { + ...defaultPaylaod, + resolvedAs, + error, + results, + }, + }); + rerender(TestApp); expect( - store.getState().uiSettings.editorPopoverResolvedData.results.length - ).toEqual(0); + store.getState().uiSettings.editorPopoverResolvedData.results + ).toBeUndefined(); expect( store.getState().uiSettings.editorPopoverResolvedData.resolvedAs - ).toBeUndefined(); - expect(result).toBeNull(); - }); - // case-1: url is valid and link resolved by the project resolver - it('should show popover when the link is resolved by the project resolver if the url is valid', async () => { - const orgProject = getOrgAndProjectFromResourceObject(resourceResolverApi); - const name = getResourceLabel(resourceResolverApi); - await waitFor(async () => { - await resolveLinkInEditor({ - nexus, - defaultPaylaod: { ...defaultPaylaod, top: 400, left: 400 }, - url: resourceResolverApi['@id'], - dispatch: store.dispatch, - orgLabel: orgProject?.orgLabel, - projectLabel: orgProject?.projectLabel, - }); - expect( - store.getState().uiSettings.editorPopoverResolvedData.results - ).toBeDefined(); - expect( - store.getState().uiSettings.editorPopoverResolvedData.results._self - ).toEqual(resourceResolverApi._self); - expect( - store.getState().uiSettings.editorPopoverResolvedData.resolvedAs - ).toBe('resource'); - expect( - store.getState().uiSettings.editorPopoverResolvedData.error - ).toBeNull(); - }); - await waitFor( - async () => { - const nameMatch = new RegExp(name, 'i'); - const nameInScreen = await screen.findByText(nameMatch); - expect(nameInScreen).toBeInTheDocument(); - }, - { timeout: 3000 } - ); + ).toEqual('error'); }); - // case-2: link can not be resolved by the project resolver - // then try to find it across all projects - it('should show popover when the link is resolved by search api and resolver can not resolve it if the url is valid', async () => { + // case-resources: link can be resolved by search api and has multiple results + it('should show popover when the link is resolved by search api with multiple results', async () => { const orgProject = { orgLabel: 'bbp', projectLabel: 'lnmce', }; await waitFor(async () => { - await resolveLinkInEditor({ + const { resolvedAs, error, results } = await editorLinkResolutionHandler({ nexus, - defaultPaylaod: { ...defaultPaylaod, top: 400, left: 400 }, url: resourceFromSearchApiId, - dispatch: store.dispatch, - orgLabel: orgProject.orgLabel, - projectLabel: orgProject.projectLabel, + orgLabel: orgProject?.orgLabel, + projectLabel: orgProject?.projectLabel, }); + store.dispatch({ + type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, + payload: { + ...defaultPaylaod, + resolvedAs, + error, + results, + }, + }); + rerender(TestApp); expect( store.getState().uiSettings.editorPopoverResolvedData.results ).toBeDefined(); @@ -194,18 +193,26 @@ describe('resolveLinkInEditor', () => { } }); }); - // case-3: link can not be resolved by the project resolver and search api and it's an external link - it('shoudl show popover when external link is provided', async () => { + // case-external: link can not be resolved by the project resolver nor the search api and it's external + it('should show popover when external link is provided', async () => { const url = 'ftp://www.google.com'; await waitFor(async () => { - await resolveLinkInEditor({ + const { resolvedAs, error, results } = await editorLinkResolutionHandler({ nexus, url, - defaultPaylaod: { ...defaultPaylaod, top: 400, left: 400 }, - dispatch: store.dispatch, orgLabel: 'orgLabel', projectLabel: 'projectLabel', }); + store.dispatch({ + type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, + payload: { + ...defaultPaylaod, + resolvedAs, + error, + results, + }, + }); + rerender(TestApp); expect( store.getState().uiSettings.editorPopoverResolvedData.results._self ).toEqual(url); From 88139c2561134c9e44ee9b79a6ddf243448340ac Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 6 Jul 2023 17:00:09 +0200 Subject: [PATCH 088/192] de-13/refactor: add getTokenAndPosAt for popover text and position --- .../components/ResourceEditor/editorUtils.ts | 29 ++++++++- .../components/ResourceEditor/index.tsx | 60 ++++++++----------- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index 7eea43021..37eb87adf 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -1,6 +1,7 @@ import { extractFieldName } from './../../../subapps/search/containers/FilterOptions'; import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { has, isArray, last } from 'lodash'; +import { useDispatch } from 'react-redux'; import useResolvedLinkEditorPopover from '../../molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover'; import isValidUrl, { isExternalLink, @@ -15,8 +16,6 @@ import { } from '../../utils'; import { TDELink, TDEResource } from '../../store/reducers/data-explorer'; import { UISettingsActionTypes } from '../../store/actions/ui-settings'; -import { Dispatch } from 'redux'; -import { useDispatch } from 'react-redux'; type TDeltaError = Error & { '@type': string; @@ -71,7 +70,33 @@ export const getDataExplorerResourceItemArray = ( data._rev, ]) as TDEResource; }; +const NEAR_BY = [0, 0, 0, 5, 0, -5, 5, 0, -5, 0]; +export function getTokenAndPosAt(e: MouseEvent, current: CodeMirror.Editor) { + const node = e.target || e.srcElement; + const text = + (node as HTMLElement).innerText || (node as HTMLElement).textContent; + for (let i = 0; i < NEAR_BY.length; i += 2) { + const coords = { + left: e.pageX + NEAR_BY[i], + top: e.pageY + NEAR_BY[i + 1], + }; + const pos = current.coordsChar({ + ...coords, + }); + const token = current.getTokenAt(pos); + if (token && token.string === text) { + return { + token, + coords, + }; + } + } + return { + token: null, + coords: { left: e.pageX, top: e.pageY }, + }; +} export async function editorLinkResolutionHandler({ nexus, orgLabel, diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 8053e1b83..1caec99a8 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -23,6 +23,7 @@ import { mayBeResolvableLink, useResourceResoultion, editorLinkResolutionHandler, + getTokenAndPosAt, } from './editorUtils'; import { RootState } from '../../store/reducers'; import './ResourceEditor.less'; @@ -134,42 +135,33 @@ const ResourceEditor: React.FunctionComponent = props => { } }); }; + const onLinkClick = async (_: any, ev: MouseEvent) => { - setLoadingResolution(true); - const x = ev.pageX; - const y = ev.pageY; - const editorPosition = codeMirorRef.current?.coordsChar({ - left: x, - top: y, - }); - const token = (editorPosition - ? codeMirorRef.current?.getTokenAt(editorPosition) - : { start: 0, end: 0, string: '' }) as codemiror.Token; - const tokenStart = editorPosition?.ch || 0; - // const left = x - ((tokenStart - token.start) * 8); - const left = x - LINE_HEIGHT; - const top = y - LINE_HEIGHT; - const defaultPaylaod = { top, left, open: true }; - // replace the double quotes in the borns of the string because code mirror will added another double quotes - // and it will break the url - const url = (token as codemiror.Token).string - .replace(/\\/g, '') - .replace(/\"/g, ''); - if (mayBeResolvableLink(url)) { - const { resolvedAs, results, error } = await editorLinkResolutionHandler({ - nexus, - url, - orgLabel, - projectLabel, - }); - onResolutionComplete({ - ...defaultPaylaod, - resolvedAs, - results, - error, - }); + if (codeMirorRef.current) { + setLoadingResolution(true); + const { coords, token } = getTokenAndPosAt(ev, codeMirorRef.current); + const url = token?.string.replace(/\\/g, '').replace(/\"/g, ''); + if (url && mayBeResolvableLink(url)) { + const position = { ...coords, open: true }; + const { + resolvedAs, + results, + error, + } = await editorLinkResolutionHandler({ + nexus, + url, + orgLabel, + projectLabel, + }); + onResolutionComplete({ + ...position, + resolvedAs, + results, + error, + }); + } + setLoadingResolution(false); } - setLoadingResolution(false); }; React.useEffect(() => { From 33d22f2d3836d25fd7fe02573f6c5d99d78dc903 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 6 Jul 2023 17:35:55 +0200 Subject: [PATCH 089/192] de-13/fix: test --- .../ResourceEditor/editorUtils.spec.tsx | 3 - .../components/ResourceEditor/editorUtils.ts | 64 ++++++++++--------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.spec.tsx b/src/shared/components/ResourceEditor/editorUtils.spec.tsx index 94d51894e..90ed9e968 100644 --- a/src/shared/components/ResourceEditor/editorUtils.spec.tsx +++ b/src/shared/components/ResourceEditor/editorUtils.spec.tsx @@ -223,8 +223,5 @@ describe('resolveLinkInEditor', () => { store.getState().uiSettings.editorPopoverResolvedData.error ).toBeUndefined(); }); - const urlMatch = new RegExp(url, 'i'); - const urlInScreen = await screen.findByText(urlMatch); - expect(urlInScreen).toBeInTheDocument(); }); }); diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index 37eb87adf..ab03bf4b7 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -110,7 +110,7 @@ export async function editorLinkResolutionHandler({ }): Promise { let data; try { - // case-1: link resolved by the project resolver + // case: link resolved by the project resolver data = await fetchResourceByResolver({ nexus, orgLabel, @@ -119,6 +119,8 @@ export async function editorLinkResolutionHandler({ }); const entity = getOrgAndProjectFromResourceObject(data); const isDownloadable = isDownloadableLink(data); + // case-resource: link is resolved as a resource by project resolver + // next-action: open resource editor return { resolvedAs: 'resource', results: { @@ -134,20 +136,17 @@ export async function editorLinkResolutionHandler({ }; } catch (error) { try { - // case-2: link can not be resolved by the project resolver - // then try to find it across all projects - // it may be single resource, multiple resources or external resource - // if no resource found then we consider it as an error + // cases: using nexus search api to resolve the link data = await nexus.Resource.list(undefined, undefined, { locate: url, }); - if ( - !data._total || - (!data._total && url.startsWith('https://bbp.epfl.ch')) - ) { + if (!data._total || (!data._total && isExternalLink(url))) { + // case-error: link is not resolved by nither project resolver nor nexus search api + // next-action: throw error and capture it in the catch block throw new Error('Resource can not be resolved'); - } - if (data._total === 1) { + } else if (data._total === 1) { + // case-resource: link is resolved as a resource by nexus search api + // next-action: open resource editor const result = data._results[0]; const isDownloadable = isDownloadableLink(result); const entity = getOrgAndProjectFromResourceObject(result); @@ -164,27 +163,29 @@ export async function editorLinkResolutionHandler({ ), }, }; + } else { + // case-resources: link is resolved as a list of resources by nexus search api + // next-action: open resources list in the popover + return { + resolvedAs: 'resources', + results: data._results.map(item => { + const isDownloadable = isDownloadableLink(item); + const entity = getOrgAndProjectFromResourceObject(item); + return { + isDownloadable, + _self: item._self, + title: getResourceLabel(item), + types: getNormalizedTypes(item['@type']), + resource: getDataExplorerResourceItemArray( + entity ?? { orgLabel: '', projectLabel: '' }, + item + ), + }; + }), + }; } - return { - resolvedAs: 'resources', - results: data._results.map(item => { - const isDownloadable = isDownloadableLink(item); - const entity = getOrgAndProjectFromResourceObject(item); - return { - isDownloadable, - _self: item._self, - title: getResourceLabel(item), - types: getNormalizedTypes(item['@type']), - resource: getDataExplorerResourceItemArray( - entity ?? { orgLabel: '', projectLabel: '' }, - item - ), - }; - }), - }; } catch (error) { - // case-3: if an error occured when tring both resolution method above - // we check if the resource is external + // case-external: link is external if (isExternalLink(url)) { return { resolvedAs: 'external', @@ -195,7 +196,8 @@ export async function editorLinkResolutionHandler({ }, }; } - // case-4: if not an external url then it will be an error + // case-error: link is not resolved by nither project resolver nor nexus search api + // and it's not an external link return { error: has(error, 'details') ? (error as TDeltaError).details From ff8ae6e98ab52d1b9e4e9df106f8709561aea12b Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Fri, 7 Jul 2023 09:16:56 +0200 Subject: [PATCH 090/192] de-13/clean --- src/shared/components/ResourceEditor/editorUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index ab03bf4b7..fd9bd53cc 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -1,4 +1,3 @@ -import { extractFieldName } from './../../../subapps/search/containers/FilterOptions'; import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { has, isArray, last } from 'lodash'; import { useDispatch } from 'react-redux'; From 670a60a2b748062cb4c74b9b2c7aa670618d9afb Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:13:51 +0200 Subject: [PATCH 091/192] de-14/pkg: add lru-cache and configure jest for the module --- jest.setup.js | 13 +++++++++++++ package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/jest.setup.js b/jest.setup.js index 68a36ba63..70b0d0273 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,3 +1,4 @@ +const { rest } = require('msw'); fetch = require('node-fetch'); window.fetch = fetch; @@ -23,3 +24,15 @@ jest.mock('resize-observer-polyfill', () => ({ disconnect: jest.fn(), })), })); + +jest.mock('lru-cache', () => { + return { + __esModule: true, + default: jest.fn().mockImplementation(() => { + return ({ + fetch: jest.fn(), + clear: jest.fn(), + }) + }) + } +}); \ No newline at end of file diff --git a/package.json b/package.json index 80c4ce8d3..e1a1e38df 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "jwt-decode": "^2.2.0", "localforage": "^1.9.0", "lodash": "^4.17.21", + "lru-cache": "7.18.3", "moment": "^2.29.4", "morgan": "^1.9.1", "motion": "^10.15.5", diff --git a/yarn.lock b/yarn.lock index 06f2d1b8c..2e4929556 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12645,6 +12645,11 @@ lowlight@~1.11.0: fault "^1.0.2" highlight.js "~9.13.0" +lru-cache@7.18.3: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" From d40321d77ada441527db4032b7c17c529cd88150 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:15:04 +0200 Subject: [PATCH 092/192] de-14/update: add LRU cache utils and update the editor resolution fn --- .../ResourceEditor/ResourcesLRUCache.ts | 85 +++++++ .../components/ResourceEditor/editorUtils.ts | 210 ++++++++++-------- 2 files changed, 207 insertions(+), 88 deletions(-) create mode 100644 src/shared/components/ResourceEditor/ResourcesLRUCache.ts diff --git a/src/shared/components/ResourceEditor/ResourcesLRUCache.ts b/src/shared/components/ResourceEditor/ResourcesLRUCache.ts new file mode 100644 index 000000000..698bdf927 --- /dev/null +++ b/src/shared/components/ResourceEditor/ResourcesLRUCache.ts @@ -0,0 +1,85 @@ +// NOTE: This file will be removed when delta introduce http cache headers +import { NexusClient, Resource, PaginatedList } from '@bbp/nexus-sdk'; +import LRUCache from 'lru-cache'; + +// TODO: Use nexus.httpGet to prepare for using http cache headers +// since the nexus SDK can not accept the headers as an argument +const lookByProjectResolver = async ({ + nexus, + apiEndpoint, + orgLabel, + projectLabel, + resourceId, +}: { + nexus: NexusClient; + apiEndpoint: string; + orgLabel: string; + projectLabel: string; + resourceId: string; +}): Promise => { + return await nexus.httpGet({ + path: `${apiEndpoint}/resolvers/${orgLabel}/${projectLabel}/_/${resourceId}`, + }); +}; +const lookBySearchApi = async ({ + nexus, + apiEndpoint, + resourceId, +}: { + nexus: NexusClient; + apiEndpoint: string; + resourceId: string; +}): Promise => { + return await nexus.httpGet({ + path: `${apiEndpoint}/resources?locate=${resourceId}`, + }); +}; + +export type TPagedResources = PaginatedList & { + [key: string]: any; +}; +export type TResolutionType = 'resolver-api' | 'search-api' | 'error'; +export type TResolutionData = Resource | TPagedResources | Error; +export type TResolutionReturnedData = { + data: TResolutionData; + type: TResolutionType; +}; +export type ResourceResolutionFetchFn = ( + key: string, + { fetchContext }: { fetchContext: any } +) => Promise; +type Options = LRUCache.Options; + +const Options: Options = { + max: 100, + ttl: 1000 * 60 * 30, // 30 minutes + fetchMethod: async ( + _, + undefined, + options: LRUCache.FetcherOptions + ) => { + try { + return { + data: await lookByProjectResolver(options.context), + type: 'resolver-api', + }; + } catch (error) { + try { + return { + data: await lookBySearchApi(options.context), + type: 'search-api', + }; + } catch (error) { + throw { + data: error, + type: 'error', + }; + } + } + }, +}; +const ResourceResolutionCache = new LRUCache( + Options +); + +export default ResourceResolutionCache; diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index fd9bd53cc..fe3ace48c 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -1,5 +1,5 @@ import { NexusClient, Resource } from '@bbp/nexus-sdk'; -import { has, isArray, last } from 'lodash'; +import { has } from 'lodash'; import { useDispatch } from 'react-redux'; import useResolvedLinkEditorPopover from '../../molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover'; import isValidUrl, { @@ -7,14 +7,21 @@ import isValidUrl, { isStorageLink, isUrlCurieFormat, } from '../../../utils/validUrl'; -import { fetchResourceByResolver } from '../../../subapps/admin/components/Settings/ResolversSubView'; import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; import { + getNormalizedTypes, getOrgAndProjectFromResourceObject, getResourceLabel, } from '../../utils'; import { TDELink, TDEResource } from '../../store/reducers/data-explorer'; import { UISettingsActionTypes } from '../../store/actions/ui-settings'; +import ResourceResolutionCache, { + ResourceResolutionFetchFn, + TPagedResources, + TResolutionData, + TResolutionReturnedData, + TResolutionType, +} from './ResourcesLRUCache'; type TDeltaError = Error & { '@type': string; @@ -28,25 +35,14 @@ type TReturnedResolvedData = Omit< 'top' | 'left' | 'open' >; +export const LINE_HEIGHT = 15; +export const INDENT_UNIT = 4; +const NEAR_BY = [0, 0, 0, 5, 0, -5, 5, 0, -5, 0]; const isDownloadableLink = (resource: Resource) => { return Boolean( resource['@type'] === 'File' || resource['@type']?.includes('File') ); }; -export const getNormalizedTypes = (types?: string | string[]) => { - if (types) { - if (isArray(types)) { - return types.map(item => { - if (isValidUrl(item)) { - return item.split('/').pop()!; - } - return item; - }); - } - return [last(types.split('/'))!]; - } - return []; -}; export const mayBeResolvableLink = (url: string): boolean => { return isValidUrl(url) && !isUrlCurieFormat(url) && !isStorageLink(url); }; @@ -69,84 +65,107 @@ export const getDataExplorerResourceItemArray = ( data._rev, ]) as TDEResource; }; -const NEAR_BY = [0, 0, 0, 5, 0, -5, 5, 0, -5, 0]; export function getTokenAndPosAt(e: MouseEvent, current: CodeMirror.Editor) { const node = e.target || e.srcElement; const text = (node as HTMLElement).innerText || (node as HTMLElement).textContent; - + const editorRect = (e.target as HTMLElement).getBoundingClientRect(); for (let i = 0; i < NEAR_BY.length; i += 2) { const coords = { left: e.pageX + NEAR_BY[i], top: e.pageY + NEAR_BY[i + 1], }; - const pos = current.coordsChar({ - ...coords, - }); + const pos = current.coordsChar(coords); const token = current.getTokenAt(pos); if (token && token.string === text) { return { token, - coords, + coords: { + left: editorRect.left, + top: coords.top + LINE_HEIGHT, + }, }; } } return { token: null, - coords: { left: e.pageX, top: e.pageY }, + coords: { left: editorRect.left, top: e.pageY }, }; } export async function editorLinkResolutionHandler({ nexus, + apiEndpoint, orgLabel, projectLabel, url, + fetcher, }: { nexus: NexusClient; + apiEndpoint: string; url: string; orgLabel: string; projectLabel: string; + fetcher?: ResourceResolutionFetchFn; }): Promise { - let data; - try { - // case: link resolved by the project resolver - data = await fetchResourceByResolver({ - nexus, - orgLabel, - projectLabel, - resourceId: encodeURIComponent(url), - }); - const entity = getOrgAndProjectFromResourceObject(data); - const isDownloadable = isDownloadableLink(data); - // case-resource: link is resolved as a resource by project resolver - // next-action: open resource editor - return { - resolvedAs: 'resource', - results: { - isDownloadable, - _self: data._self, - title: getResourceLabel(data), - types: getNormalizedTypes(data['@type']), - resource: getDataExplorerResourceItemArray( - entity ?? { orgLabel: '', projectLabel: '' }, - data - ), + const key = `${orgLabel}/${projectLabel}/${url}`; + let data: TResolutionData; + let type: TResolutionType; + if (fetcher) { + ({ data, type } = await fetcher(key, { + fetchContext: { + nexus, + apiEndpoint, + orgLabel, + projectLabel, + resourceId: encodeURIComponent(url), }, - }; - } catch (error) { - try { - // cases: using nexus search api to resolve the link - data = await nexus.Resource.list(undefined, undefined, { - locate: url, - }); - if (!data._total || (!data._total && isExternalLink(url))) { + })); + } else { + ({ data, type } = await ResourceResolutionCache.fetch(key, { + fetchContext: { + nexus, + apiEndpoint, + orgLabel, + projectLabel, + resourceId: encodeURIComponent(url), + }, + })); + } + switch (type) { + case 'resolver-api': { + const details: Resource = data as Resource; + const entity = getOrgAndProjectFromResourceObject(details); + const isDownloadable = isDownloadableLink(details); + // case-resource: link is resolved as a resource by project resolver + // next-action: open resource editor + return { + resolvedAs: 'resource', + results: { + isDownloadable, + _self: details._self, + title: getResourceLabel(details), + types: getNormalizedTypes(details['@type']), + resource: getDataExplorerResourceItemArray( + entity ?? { orgLabel: '', projectLabel: '' }, + details + ), + }, + }; + } + case 'search-api': { + const details = data as TPagedResources; + if (!details._total || (!details._total && isExternalLink(url))) { // case-error: link is not resolved by nither project resolver nor nexus search api // next-action: throw error and capture it in the catch block - throw new Error('Resource can not be resolved'); - } else if (data._total === 1) { + return { + error: 'Resource can not be resolved', + resolvedAs: 'error', + }; + } + if (details._total === 1) { // case-resource: link is resolved as a resource by nexus search api // next-action: open resource editor - const result = data._results[0]; + const result = details._results[0]; const isDownloadable = isDownloadableLink(result); const entity = getOrgAndProjectFromResourceObject(result); return { @@ -162,29 +181,30 @@ export async function editorLinkResolutionHandler({ ), }, }; - } else { - // case-resources: link is resolved as a list of resources by nexus search api - // next-action: open resources list in the popover - return { - resolvedAs: 'resources', - results: data._results.map(item => { - const isDownloadable = isDownloadableLink(item); - const entity = getOrgAndProjectFromResourceObject(item); - return { - isDownloadable, - _self: item._self, - title: getResourceLabel(item), - types: getNormalizedTypes(item['@type']), - resource: getDataExplorerResourceItemArray( - entity ?? { orgLabel: '', projectLabel: '' }, - item - ), - }; - }), - }; } - } catch (error) { - // case-external: link is external + // case-resources: link is resolved as a list of resources by nexus search api + // next-action: open resources list in the popover + return { + resolvedAs: 'resources', + results: details._results.map((item: Resource) => { + const isDownloadable = isDownloadableLink(item); + const entity = getOrgAndProjectFromResourceObject(item); + return { + isDownloadable, + _self: item._self, + title: getResourceLabel(item), + types: getNormalizedTypes(item['@type']), + resource: getDataExplorerResourceItemArray( + entity ?? { orgLabel: '', projectLabel: '' }, + item + ), + }; + }), + }; + } + case 'error': + default: { + const details = data as any; if (isExternalLink(url)) { return { resolvedAs: 'external', @@ -198,9 +218,9 @@ export async function editorLinkResolutionHandler({ // case-error: link is not resolved by nither project resolver nor nexus search api // and it's not an external link return { - error: has(error, 'details') - ? (error as TDeltaError).details - : (error as TErrorMessage).message ?? JSON.stringify(error), + error: has(details, 'details') + ? (details as TDeltaError).details + : (details as TErrorMessage).message ?? JSON.stringify(details), resolvedAs: 'error', }; } @@ -209,7 +229,11 @@ export async function editorLinkResolutionHandler({ export const useResourceResoultion = () => { const dispatch = useDispatch(); - const { navigateResourceHandler } = useResolvedLinkEditorPopover(); + const { + navigateResourceHandler, + downloadBinaryAsyncHandler, + } = useResolvedLinkEditorPopover(); + return ({ resolvedAs, error, @@ -217,8 +241,18 @@ export const useResourceResoultion = () => { top, left, }: TEditorPopoverResolvedData) => { - if (resolvedAs === 'resource' && !(results as TDELink).isDownloadable) { - return navigateResourceHandler({ ...(results as TDELink) }); + if (resolvedAs === 'resource') { + 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 navigateResourceHandler(result); } if (resolvedAs === 'external') { return window.open( @@ -230,11 +264,11 @@ export const useResourceResoultion = () => { return dispatch({ type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, payload: { + top, + left, resolvedAs, error, results, - top, - left, open: true, }, }); From 52efd4ef85912d9181336dda57eb7fa9a2f83562 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:16:56 +0200 Subject: [PATCH 093/192] de-14/refactor: remove all cases for popover since they will be handled by the tooltip --- src/shared/containers/ResourceEditor.tsx | 15 ++-- .../ResolvedLinkEditorPopover.tsx | 84 +++++-------------- .../ResolvedLinkEditorPopover/styles.less | 24 +++--- .../useResolvedLinkEditorPopover.tsx | 11 ++- 4 files changed, 47 insertions(+), 87 deletions(-) diff --git a/src/shared/containers/ResourceEditor.tsx b/src/shared/containers/ResourceEditor.tsx index bcce0ee0f..26de03c9c 100644 --- a/src/shared/containers/ResourceEditor.tsx +++ b/src/shared/containers/ResourceEditor.tsx @@ -7,19 +7,20 @@ import { } from '@bbp/nexus-sdk'; import { useHistory, useLocation } from 'react-router'; import { useDispatch } from 'react-redux'; -import ResourceEditor from '../components/ResourceEditor'; +import { pick } from 'lodash'; import { useNexusContext } from '@bbp/react-nexus'; -import { - getDataExplorerResourceItemArray, - getNormalizedTypes, -} from '../components/ResourceEditor/editorUtils'; +import ResourceEditor from '../components/ResourceEditor'; +import { getDataExplorerResourceItemArray } from '../components/ResourceEditor/editorUtils'; import useNotification, { parseNexusError } from '../hooks/useNotification'; import { InitDataExplorerGraphFlowLimitedVersion, InitNewVisitDataExplorerGraphView, } from '../store/reducers/data-explorer'; -import { getOrgAndProjectFromResourceObject, getResourceLabel } from '../utils'; -import { pick } from 'lodash'; +import { + getNormalizedTypes, + getOrgAndProjectFromResourceObject, + getResourceLabel, +} from '../utils'; const ResourceEditorContainer: React.FunctionComponent<{ resourceId: string; diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx index dd9543e91..1a8bcc165 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx @@ -1,11 +1,10 @@ -import React, { ReactNode, useRef, forwardRef } from 'react'; +import React, { ReactNode, useRef, forwardRef, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { useMutation } from 'react-query'; import { clsx } from 'clsx'; import { Tag, notification } from 'antd'; import * as Sentry from '@sentry/browser'; import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; -import { match as pmatch } from 'ts-pattern'; import { RootState } from '../../store/reducers'; import { TDELink } from '../../store/reducers/data-explorer'; import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; @@ -73,8 +72,6 @@ const ResolvedLinkEditorPopover = () => { editorPopoverResolvedData: { open, error, resolvedAs, results }, } = useSelector((state: RootState) => state.uiSettings); - const resultPattern: TResultPattern = { resolvedAs, open }; - const { mutateAsync: downloadBinaryAsync, isLoading: downloadInProgress, @@ -93,81 +90,40 @@ const ResolvedLinkEditorPopover = () => { }, }); - return pmatch(resultPattern) - .with({ open: true, resolvedAs: 'error' }, () => ( + if (resolvedAs === 'resources' && open) { + return ( -
- Error -
{error}
-
-
- )) - .with({ open: true, resolvedAs: 'resource' }, () => { - const result = results as TDELink; - return ( - -
- {result.resource?.[0] && result.resource?.[1] && ( - {`${result.resource?.[0]}/${result.resource?.[1]}`} + {(results as TDELink[]).map(item => ( +
+ {item.resource?.[0] && item.resource?.[1] && ( + {`${item.resource?.[0]}/${item.resource?.[1]}`} )} - {result.isDownloadable && ( + {item.isDownloadable && ( downloadBinaryAsync({ - orgLabel: result.resource?.[0]!, - projectLabel: result.resource?.[1]!, - resourceId: result.resource?.[2]!, - ext: result.resource?.[4] ?? 'json', - title: result.title, + orgLabel: item.resource?.[0]!, + projectLabel: item.resource?.[1]!, + resourceId: item.resource?.[2]!, + ext: item.resource?.[4] ?? 'json', + title: item.title, }) } /> )}
- - ); - }) - .with({ open: true, resolvedAs: 'resources' }, () => { - return ( - - {(results as TDELink[]).map(item => ( -
- {item.resource?.[0] && item.resource?.[1] && ( - {`${item.resource?.[0]}/${item.resource?.[1]}`} - )} - - {item.isDownloadable && ( - - downloadBinaryAsync({ - orgLabel: item.resource?.[0]!, - projectLabel: item.resource?.[1]!, - resourceId: item.resource?.[2]!, - ext: item.resource?.[4] ?? 'json', - title: item.title, - }) - } - /> - )} -
- ))} -
- ); - }) - .otherwise(() => <>); + ))} + + ); + } + return null; }; export default ResolvedLinkEditorPopover; diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less index 9931c3a25..b1c10a7f1 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less +++ b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less @@ -17,8 +17,6 @@ align-items: center; justify-content: flex-start; gap: 2px; - // margin-bottom: 3px; - // padding: 0 4px; width: 100%; overflow: hidden; span.ant-tag { @@ -26,18 +24,24 @@ border-radius: 4px; box-shadow: 0 2px 12px rgba(#333, 0.12); } - - &.list-item { + &.list-item:hover { + background-color: #efefef; &:first-child { - span.ant-tag { - margin: 10px 12px 5px; - } + border-top-left-radius: 4px; + border-top-right-radius: 4px; } &:last-child { - span.ant-tag { - margin: 10px 12px 10px; - } + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } + span.ant-tag { + transform: scale(1.04); + transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1); } + } + &.list-item { + padding: 2px 4px; + span.ant-tag { margin: 3px 12px; } diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover.tsx index d1f13b883..7808bfc1e 100644 --- a/src/shared/molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover.tsx +++ b/src/shared/molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover.tsx @@ -3,7 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useLocation, useRouteMatch } from 'react-router'; import { Resource } from '@bbp/nexus-sdk'; import { RootState } from '../../store/reducers'; -import { getNormalizedTypes } from '../../components/ResourceEditor/editorUtils'; import { UISettingsActionTypes } from '../../store/actions/ui-settings'; import { TDELink, @@ -11,7 +10,11 @@ import { InitNewVisitDataExplorerGraphView, } from '../../store/reducers/data-explorer'; import { editorPopoverResolvedDataInitialValue } from '../../store/reducers/ui-settings'; -import { getOrgAndProjectFromProjectId, getResourceLabel } from '../../utils'; +import { + getNormalizedTypes, + getOrgAndProjectFromProjectId, + getResourceLabel, +} from '../../utils'; import { parseResourceId } from '../../components/Preview/Preview'; import { download } from '../../utils/download'; @@ -25,10 +28,6 @@ const useResolvedLinkEditorPopover = () => { resourceId: string; }>(`/:orgLabel/:projectLabel/resources/:resourceId`); - const { - editorPopoverResolvedData: { open, error, resolvedAs, results }, - } = useSelector((state: RootState) => state.uiSettings); - const { pathname, search, state } = useLocation(); const clickOutsideHandler = () => { From e61aaf8f5b4b2a0ca2e335345061a8a926c29a8b Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:19:23 +0200 Subject: [PATCH 094/192] de-14/clean: format code --- .../OrganizationProjectsPage.spec.tsx | 10 ++-------- .../admin/components/Settings/ResolversSubView.tsx | 2 ++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pages/OrganizationProjectsPage/OrganizationProjectsPage.spec.tsx b/src/pages/OrganizationProjectsPage/OrganizationProjectsPage.spec.tsx index c8a934870..65a55cb4c 100644 --- a/src/pages/OrganizationProjectsPage/OrganizationProjectsPage.spec.tsx +++ b/src/pages/OrganizationProjectsPage/OrganizationProjectsPage.spec.tsx @@ -2,7 +2,7 @@ import '@testing-library/jest-dom'; import { renderHook } from '@testing-library/react-hooks'; import fetch from 'node-fetch'; import { act } from 'react-dom/test-utils'; -import { NexusProvider, useNexusContext } from '@bbp/react-nexus'; +import { NexusProvider } from '@bbp/react-nexus'; import { ProjectList, ProjectResponseCommon, @@ -13,13 +13,7 @@ import { createBrowserHistory } from 'history'; import { Provider } from 'react-redux'; import { ConnectedRouter } from 'connected-react-router'; -import { - render, - fireEvent, - waitFor, - screen, - server, -} from '../../utils/testUtil'; +import { render, waitFor, screen, server } from '../../utils/testUtil'; import configureStore from '../../shared/store'; import OrganizationProjectsPage, { useInfiniteOrganizationProjectsQuery, diff --git a/src/subapps/admin/components/Settings/ResolversSubView.tsx b/src/subapps/admin/components/Settings/ResolversSubView.tsx index 3584cca21..8aeead0ca 100644 --- a/src/subapps/admin/components/Settings/ResolversSubView.tsx +++ b/src/subapps/admin/components/Settings/ResolversSubView.tsx @@ -11,6 +11,8 @@ import ReactJson from 'react-json-view'; import { easyValidURL } from '../../../../utils/validUrl'; import './styles.less'; import { Link } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import { RootState } from 'shared/store/reducers'; type Props = {}; type TDataType = { From 5766a02dac5a407939a79eb76a38b701c7e94e18 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:22:16 +0200 Subject: [PATCH 095/192] de-14/update: use session storage instead of localstorge to enable per page path preserve de-14/refactor: add data explorer graph flow path as constant --- .../DateExplorerGraphFlow.tsx | 14 +++++++++++--- src/shared/routes.ts | 4 ++-- src/shared/store/reducers/data-explorer.ts | 3 ++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index e51e0aa5a..02a8fa33c 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -5,6 +5,7 @@ import { clsx } from 'clsx'; import { RootState } from '../../store/reducers'; import { DATA_EXPLORER_GRAPH_FLOW_DIGEST, + DATA_EXPLORER_GRAPH_FLOW_PATH, PopulateDataExplorerGraphFlow, ResetDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; @@ -14,6 +15,8 @@ import { } from '../../molecules/DataExplorerGraphFlowMolecules'; import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; +import ResourceResolutionCache from '../../components/ResourceEditor/ResourcesLRUCache'; + import './styles.less'; const DataExplorerGraphFlow = () => { @@ -27,7 +30,7 @@ const DataExplorerGraphFlow = () => { useEffect(() => { if (!digestFirstRender.current) { - const state = localStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + const state = sessionStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); if (state) { dispatch(PopulateDataExplorerGraphFlow(state)); } @@ -37,14 +40,19 @@ const DataExplorerGraphFlow = () => { useEffect(() => { const unlisten = history.listen(location => { - if (!location.pathname.startsWith('/data-explorer/graph-flow')) { + if (!location.pathname.startsWith(DATA_EXPLORER_GRAPH_FLOW_PATH)) { dispatch(ResetDataExplorerGraphFlow({ initialState: null })); - localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + sessionStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); } }); return () => unlisten(); }, []); + useEffect(() => { + return () => { + ResourceResolutionCache.clear(); + }; + }, [ResourceResolutionCache]); if (current === null) { return (
diff --git a/src/shared/routes.ts b/src/shared/routes.ts index 7ce53035c..cb4f2e537 100644 --- a/src/shared/routes.ts +++ b/src/shared/routes.ts @@ -6,7 +6,7 @@ import Home from '../pages/HomePage/HomePage'; import IdentityPage from '../pages/IdentityPage/IdentityPage'; import StudioRedirectView from './views/StudioRedirectView'; import MyDataPage from '../pages/MyDataPage/MyDataPage'; -import DataExplorerResolverPage from '../pages/DataExplorerGraphFlowPage/DataExplorerGraphFlowPage'; +import DataExplorerGraphFlowPage from '../pages/DataExplorerGraphFlowPage/DataExplorerGraphFlowPage'; import DataExplorerPage from '../pages/DataExplorerPage/DataExplorerPage'; type TRoutePropsExtended = RouteProps & { protected: boolean }; @@ -43,7 +43,7 @@ const routes: TRoutePropsExtended[] = [ }, { path: '/data-explorer/graph-flow', - component: DataExplorerResolverPage, + component: DataExplorerGraphFlowPage, exact: true, protected: true, }, diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 8beb9a507..f4553ec65 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -40,6 +40,7 @@ const initialState: TDataExplorerState = { highlightIndex: -1, }; +export const DATA_EXPLORER_GRAPH_FLOW_PATH = '/data-explorer/graph-flow'; export const DATA_EXPLORER_GRAPH_FLOW_DIGEST = 'data-explorer-last-navigation'; export const MAX_NAVIGATION_ITEMS_IN_STACK = 5; const calculateNewDigest = (state: TDataExplorerState) => { @@ -51,7 +52,7 @@ const calculateNewDigest = (state: TDataExplorerState) => { current: omit(clonedState.current, ['highlight']), }) ); - localStorage.setItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST, digest); + sessionStorage.setItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST, digest); }; const isShrinkable = (links: TDELink[]) => { From 1e22c312a16b58fca98801f94f9ef17f68b86d0e Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:23:20 +0200 Subject: [PATCH 096/192] de-14/update: separate test for normalize tests --- .../utils/__tests__/normalized-types.spec.ts | 33 +++++++++++++++++++ src/shared/utils/index.ts | 18 +++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/shared/utils/__tests__/normalized-types.spec.ts diff --git a/src/shared/utils/__tests__/normalized-types.spec.ts b/src/shared/utils/__tests__/normalized-types.spec.ts new file mode 100644 index 000000000..f8f807ed2 --- /dev/null +++ b/src/shared/utils/__tests__/normalized-types.spec.ts @@ -0,0 +1,33 @@ +import { getNormalizedTypes } from '..'; + +describe('getNormalizedTypes', () => { + const typesAsString = 'Resource'; + it('should return the normalized types', () => { + const result = getNormalizedTypes(typesAsString); + expect(result).toEqual(['Resource']); + }); + + const typesAsUrl = 'https://bluebrain.github.io/nexus/vocabulary/Resource'; + it('should return the normalized types', () => { + const result = getNormalizedTypes(typesAsUrl); + expect(result).toEqual(['Resource']); + }); + + const typesWithUrls = [ + 'https://bluebrain.github.io/nexus/vocabulary/Schema', + 'https://bluebrain.github.io/nexus/vocabulary/Resource', + 'https://bluebrain.github.io/nexus/vocabulary/Project', + 'Realm', + 'NeuronMorphology', + ]; + it('should return the normalized types', () => { + const result = getNormalizedTypes(typesWithUrls); + expect(result).toEqual([ + 'Schema', + 'Resource', + 'Project', + 'Realm', + 'NeuronMorphology', + ]); + }); +}); diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index c79a252d2..d2a1d8dc8 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -2,12 +2,13 @@ import { Resource, Identity } from '@bbp/nexus-sdk'; import { isMatch, isMatchWith, - isRegExp, isMatchWithCustomizer, pick, isArray, + last, } from 'lodash'; import * as moment from 'moment'; +import isValidUrl from '../../utils/validUrl'; /** * getProp utility - an alternative to lodash.get @@ -730,3 +731,18 @@ export function isNumeric(str: string | number) { !isNaN(str as any) && !isNaN(parseFloat(str)) // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... ); // ...and ensure strings of whitespace fail } + +export const getNormalizedTypes = (types?: string | string[]) => { + if (types) { + if (isArray(types)) { + return types.map(item => { + if (isValidUrl(item)) { + return item.split('/').pop()!; + } + return item; + }); + } + return [last(types.split('/'))!]; + } + return []; +}; From 7d6069f9e3ec8e070942d4d40a43bdaed33ec086 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:24:02 +0200 Subject: [PATCH 097/192] de-14/update: add tooltip hook manager to enable tooltip on link resolution --- .../ResourceEditor/useEditorTooltip.tsx | 243 ++++++++++++++++++ src/shared/images/DownloadingLoop.svg | 1 + 2 files changed, 244 insertions(+) create mode 100644 src/shared/components/ResourceEditor/useEditorTooltip.tsx create mode 100644 src/shared/images/DownloadingLoop.svg diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx new file mode 100644 index 000000000..ba6ad851d --- /dev/null +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -0,0 +1,243 @@ +import * as React from 'react'; +import CodeMirror from 'codemirror'; +import clsx from 'clsx'; +import { useNexusContext } from '@bbp/react-nexus'; +import { useSelector } from 'react-redux'; +import { + editorLinkResolutionHandler, + getTokenAndPosAt, + mayBeResolvableLink, +} from './editorUtils'; +import { TDELink } from '../../store/reducers/data-explorer'; +import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; +import { RootState } from '../../store/reducers'; + +const downloadImg = require('../../images/DownloadingLoop.svg'); +export const CODEMIRROR_HOVER_CLASS = 'CodeMirror-hover-tooltip'; +export const CODEMIRROR_LINK_CLASS = 'fusion-resource-link'; + +function createTooltipNode({ + tag, + title, + isDownloadable, +}: { + tag: string | null; + title: string; + isDownloadable?: boolean; +}) { + const tooltipItemContent = document.createElement('div'); + tooltipItemContent.className = 'CodeMirror-hover-tooltip-item'; + const nodeTag = document.createElement('div'); + nodeTag.className = 'tag'; + tag && nodeTag.appendChild(document.createTextNode(tag)); + tooltipItemContent.appendChild(nodeTag); + + const nodeTitle = document.createElement('span'); + nodeTitle.className = 'title'; + nodeTitle.appendChild(document.createTextNode(title)); + tooltipItemContent.appendChild(nodeTitle); + + const nodeDownload = isDownloadable && document.createElement('img'); + nodeDownload && nodeDownload.setAttribute('src', downloadImg); + nodeDownload && nodeDownload.classList.add('download-icon'); + nodeDownload && tooltipItemContent.appendChild(nodeDownload); + + return tooltipItemContent; +} +function createTooltipContent({ + resolvedAs, + error, + results, +}: Pick) { + const tooltipContent = document.createElement('div'); + tooltipContent.className = clsx( + `${CODEMIRROR_HOVER_CLASS}-content`, + resolvedAs && resolvedAs + ); + if (resolvedAs === 'error' && error) { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'Error', + title: error, + }) + ); + return tooltipContent; + } + if (resolvedAs === 'resource') { + const result = results as TDELink; + tooltipContent.appendChild( + createTooltipNode({ + tag: result.resource + ? `${result.resource?.[0]}/${result.resource?.[1]}` + : null, + title: result.title ?? result._self, + isDownloadable: result.isDownloadable, + }) + ); + return tooltipContent; + } + if (resolvedAs === 'resources') { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'Multiple', + title: `${ + (results as TDELink[]).length + } resources was found, click to list them`, + }) + ); + return tooltipContent; + } + if (resolvedAs === 'external') { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'External', + title: (results as TDELink).title ?? (results as TDELink)._self, + }) + ); + return tooltipContent; + } + return null; +} + +function useEditorTooltip({ + ref, + isEditing, + orgLabel, + projectLabel, +}: { + ref: React.MutableRefObject; + isEditing: boolean; + orgLabel: string; + projectLabel: string; +}) { + const nexus = useNexusContext(); + const { + config: { apiEndpoint }, + ui: { + editorPopoverResolvedData: { open: isPopoverOpen }, + }, + } = useSelector((state: RootState) => ({ + dataExplorer: state.dataExplorer, + config: state.config, + ui: state.uiSettings, + })); + + const allowTooltip = !isEditing && !isPopoverOpen; + + React.useEffect(() => { + const currentEditor = (ref as React.MutableRefObject) + ?.current; + const editorWrapper = currentEditor.getWrapperElement(); + function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { + const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + if (tooltipRect.height <= editorRect.top) { + tooltip.style.top = `${editorRect.top - tooltipRect.height}px`; + } else { + tooltip.style.top = `${editorRect.bottom}px`; + } + tooltip.style.left = `${editorRect.left}px`; + } + function removeTooltipFromDom(tooltip: HTMLDivElement) { + if (tooltip.parentNode) { + tooltip.parentNode.removeChild(tooltip); + } + } + function hideTooltip(tooltip: HTMLDivElement) { + if (!tooltip.parentNode) { + return; + } + setTimeout(() => { + removeTooltipFromDom(tooltip); + }, 300); + } + function showTooltip(content: HTMLDivElement, node: HTMLElement) { + const tooltip = document.createElement('div'); + tooltip.className = CODEMIRROR_HOVER_CLASS; + tooltip.appendChild(content); + document.body.appendChild(tooltip); + + function hide() { + if (tooltip) { + node.classList.remove('has-tooltip'); + hideTooltip(tooltip); + tooltip.remove(); + } + node.removeEventListener('mouseout', hide); + node.removeEventListener('click', hide); + } + + node.addEventListener('mouseout', hide); + node.addEventListener('click', hide); + + const pool: ReturnType = setTimeout(() => { + if (tooltip) { + hideTooltip(tooltip); + tooltip.remove(); + } + return clearTimeout(pool); + }, 3000); + + return tooltip; + } + + async function onMouseOver(ev: MouseEvent) { + const node = ev.target as HTMLElement; + if (node) { + const { token } = getTokenAndPosAt(ev, currentEditor); + const content = token?.string || ''; + const url = content.replace(/\\/g, '').replace(/\"/g, ''); + if (url && mayBeResolvableLink(url)) { + node.classList.add('wait-for-tooltip'); + const tooltips = document.getElementsByClassName( + CODEMIRROR_HOVER_CLASS + ); + tooltips && + Array.from(tooltips).forEach(tooltip => { + tooltip.remove(); + }); + editorLinkResolutionHandler({ + nexus, + apiEndpoint, + url, + orgLabel, + projectLabel, + }).then(({ resolvedAs, results, error }) => { + const tooltipContent = createTooltipContent({ + resolvedAs, + error, + results, + }); + if (tooltipContent) { + node.classList.remove('wait-for-tooltip'); + node.classList.add( + resolvedAs === 'error' + ? 'error' + : resolvedAs === 'resource' && + (results as TDELink).isDownloadable + ? 'downloadable' + : 'has-tooltip' + ); + const tooltip = showTooltip(tooltipContent, node); + const calculatePosition = (ev: MouseEvent) => + positionner(ev, tooltip); + editorWrapper.addEventListener('mousemove', calculatePosition); + } + }); + } + } + } + allowTooltip && editorWrapper.addEventListener('mouseover', onMouseOver); + !allowTooltip && + editorWrapper.removeEventListener('mouseover', onMouseOver); + return () => { + allowTooltip && + editorWrapper.removeEventListener('mouseover', onMouseOver); + }; + }, [ + (ref as React.MutableRefObject)?.current, + allowTooltip, + ]); +} + +export default useEditorTooltip; diff --git a/src/shared/images/DownloadingLoop.svg b/src/shared/images/DownloadingLoop.svg new file mode 100644 index 000000000..c1a71f086 --- /dev/null +++ b/src/shared/images/DownloadingLoop.svg @@ -0,0 +1 @@ + \ No newline at end of file From 02114699a741e7c0e0cd489f07eb019dac1bf0e2 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:27:26 +0200 Subject: [PATCH 098/192] de-14/update: use tooltip hook and clean resource resolution cache if not data explorer GF location --- .../components/ResourceEditor/CodeEditor.tsx | 2 +- .../ResourceEditor/ResourceEditor.less | 94 ++++++++++++++++++- .../components/ResourceEditor/index.tsx | 82 ++++++++++------ 3 files changed, 148 insertions(+), 30 deletions(-) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 5c893cb04..7a4c24c8c 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -1,7 +1,7 @@ import React, { forwardRef } from 'react'; import codemiror, { EditorConfiguration } from 'codemirror'; import { UnControlled as CodeMirror } from 'react-codemirror2'; -import { INDENT_UNIT } from '.'; +import { INDENT_UNIT } from './editorUtils'; import { clsx } from 'clsx'; import { Spin } from 'antd'; diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index b1da9b5c1..4d85d8b4e 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -17,6 +17,7 @@ color: @text-color; } } + .resource-editor .full-screen-mode { .CodeMirror { height: calc(100vh - 200px) !important; @@ -62,8 +63,20 @@ .code-mirror-editor { .fusion-resource-link { - text-decoration: underline; - cursor: pointer; + color: #0974ca !important; + cursor: pointer !important; + + &.wait-for-tooltip { + cursor: progress !important; + } + &.has-tooltip { + cursor: pointer !important; + } + &.error { + cursor: not-allowed !important; + } + &.downloadable { + } &::after { content: ''; display: inline-block; @@ -75,6 +88,7 @@ margin-left: 10px; } } + .CodeMirror-lines { cursor: text; } @@ -88,3 +102,79 @@ } } } + +.CodeMirror-hover-tooltip { + background-color: white; + border: 1px solid 333; + box-shadow: 0 2px 12px rgba(#333, 0.12); + border-radius: 4px; + font-size: 10pt; + overflow: hidden; + position: fixed; + z-index: 9999; + max-width: 600px; + white-space: pre-wrap; + transition: all 0.4s ease-in-out; + padding: 4px 0; + + &-content { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + row-gap: 2px; + + &.error { + .tag { + background-color: @fusion-danger-color; + } + } + + &.external { + .tag { + background-color: @fusion-warning-color; + color: #333; + } + } + + &.resource { + .tag { + background-color: @fusion-primary-color; + } + } + + &.resources { + .tag { + background-color: @fusion-primary-color; + } + } + } + + &-item { + display: flex; + align-items: center; + justify-content: center; + padding: 2px 10px 2px 5px; + overflow: hidden; + + .tag { + color: white; + padding: 2px 5px; + border-radius: 4px; + margin-right: 5px; + box-shadow: 0 2px 12px rgba(#333, 0.12); + } + + .title { + font-weight: 200; + font-size: 13px; + } + + .download-icon { + width: 24px; + height: 24px; + margin-left: 10px; + color: @fusion-primary-color; + } + } +} diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 1caec99a8..cb93d64f5 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Button, Switch } from 'antd'; +import { useLocation } from 'react-router'; import { CheckCircleOutlined, ExclamationCircleOutlined, @@ -26,6 +27,10 @@ import { getTokenAndPosAt, } from './editorUtils'; import { RootState } from '../../store/reducers'; +import useEditorTooltip, { CODEMIRROR_LINK_CLASS } from './useEditorTooltip'; +import { DATA_EXPLORER_GRAPH_FLOW_PATH } from '../../store/reducers/data-explorer'; +import ResourceResolutionCache from './ResourcesLRUCache'; + import './ResourceEditor.less'; export interface ResourceEditorProps { @@ -47,8 +52,6 @@ export interface ResourceEditorProps { onFullScreen(): void; } -export const LINE_HEIGHT = 50; -export const INDENT_UNIT = 4; const switchMarginRight = { marginRight: 5 }; const isClickableLine = (url: string) => { @@ -74,7 +77,9 @@ const ResourceEditor: React.FunctionComponent = props => { onFullScreen, showControlPanel = true, } = props; + const nexus = useNexusContext(); + const location = useLocation(); const onResolutionComplete = useResourceResoultion(); const [loadingResolution, setLoadingResolution] = React.useState(false); const [isEditing, setEditing] = React.useState(editing); @@ -86,9 +91,11 @@ const ResourceEditor: React.FunctionComponent = props => { const { dataExplorer: { limited }, oidc, + config: { apiEndpoint }, } = useSelector((state: RootState) => ({ dataExplorer: state.dataExplorer, oidc: state.oidc, + config: state.config, })); const userAuthenticated = oidc.user && oidc.user.access_token; const keyFoldCode = (cm: any) => { @@ -127,40 +134,46 @@ const ResourceEditor: React.FunctionComponent = props => { }; const onLinksFound = () => { const elements = document.getElementsByClassName('cm-string'); - Array.from(elements).forEach(item => { + Array.from(elements).forEach((item, index) => { const itemSpan = item as HTMLSpanElement; const url = itemSpan.innerText.replace(/^"|"$/g, ''); if (isClickableLine(url)) { - itemSpan.classList.add('fusion-resource-link'); + itemSpan.classList.add(CODEMIRROR_LINK_CLASS); } }); }; const onLinkClick = async (_: any, ev: MouseEvent) => { if (codeMirorRef.current) { - setLoadingResolution(true); - const { coords, token } = getTokenAndPosAt(ev, codeMirorRef.current); - const url = token?.string.replace(/\\/g, '').replace(/\"/g, ''); - if (url && mayBeResolvableLink(url)) { - const position = { ...coords, open: true }; - const { - resolvedAs, - results, - error, - } = await editorLinkResolutionHandler({ - nexus, - url, - orgLabel, - projectLabel, - }); - onResolutionComplete({ - ...position, - resolvedAs, - results, - error, - }); + try { + setLoadingResolution(true); + const { coords, token } = getTokenAndPosAt(ev, codeMirorRef.current); + const url = token?.string.replace(/\\/g, '').replace(/\"/g, ''); + if (url && mayBeResolvableLink(url)) { + ev.stopPropagation(); + const { + resolvedAs, + results, + error, + } = await editorLinkResolutionHandler({ + nexus, + apiEndpoint, + url, + orgLabel, + projectLabel, + }); + onResolutionComplete({ + ...coords, + resolvedAs, + results, + error, + open: true, + }); + } + } catch (error) { + } finally { + setLoadingResolution(false); } - setLoadingResolution(false); } }; @@ -202,6 +215,21 @@ const ResourceEditor: React.FunctionComponent = props => { setEditing(false); }; + useEditorTooltip({ + orgLabel, + projectLabel, + isEditing, + ref: codeMirorRef, + }); + + React.useEffect(() => { + return () => { + if (location.pathname !== DATA_EXPLORER_GRAPH_FLOW_PATH) { + ResourceResolutionCache.clear(); + } + }; + }, [ResourceResolutionCache, location]); + return (
= props => { )} = props => { loadingResolution={loadingResolution} onLinkClick={onLinkClick} onLinksFound={onLinksFound} - ref={codeMirorRef} fullscreen={limited} />
From c3cd83eb6ff97cf69558b6d2bb3c6a0fb7c56608 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Mon, 10 Jul 2023 21:27:53 +0200 Subject: [PATCH 099/192] de-14/update: test for editor utils and resource editor --- .../ResourceEditor/ResourceEditor.spec.tsx | 1 + .../ResourceEditor/editorUtils.spec.tsx | 113 +++++------------- 2 files changed, 34 insertions(+), 80 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx index f1c15ea70..6d913fd89 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx +++ b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx @@ -20,6 +20,7 @@ document.createRange = () => { return range; }; + describe('ResourceEditor', () => { it('check if code editor will be rendered in the screen', async () => { const editor = React.createRef(); diff --git a/src/shared/components/ResourceEditor/editorUtils.spec.tsx b/src/shared/components/ResourceEditor/editorUtils.spec.tsx index 90ed9e968..cae3c1bf0 100644 --- a/src/shared/components/ResourceEditor/editorUtils.spec.tsx +++ b/src/shared/components/ResourceEditor/editorUtils.spec.tsx @@ -1,22 +1,19 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { getNormalizedTypes, editorLinkResolutionHandler } from './editorUtils'; +import { editorLinkResolutionHandler } from './editorUtils'; import { Provider } from 'react-redux'; import { createMemoryHistory } from 'history'; import { NexusClient, createNexusClient } from '@bbp/nexus-sdk'; -import { deltaPath } from '__mocks__/handlers/handlers'; -import configureStore from '../../../shared/store'; import { Router } from 'react-router-dom'; import { NexusProvider } from '@bbp/react-nexus'; import { AnyAction, Store } from 'redux'; import { QueryClientProvider, QueryClient } from 'react-query'; -import { setupServer } from 'msw/node'; +import configureStore from '../../store'; import { resourceFromSearchApiId, resourceFromSearchApi, - getResolverResponseObject, - getSearchApiResponseObject, } from '../../../__mocks__/handlers/ResourceEditor/handlers'; +import { deltaPath } from '../../../__mocks__/handlers/handlers'; import { getResourceLabel } from '../../utils'; import { render, @@ -26,7 +23,8 @@ import { RenderResult, } from '../../../utils/testUtil'; import ResolvedLinkEditorPopover from '../../molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover'; -import { UISettingsActionTypes } from 'shared/store/actions/ui-settings'; +import { UISettingsActionTypes } from '../../store/actions/ui-settings'; +import { ResourceResolutionFetchFn } from './ResourcesLRUCache'; document.createRange = () => { const range = new Range(); @@ -43,53 +41,40 @@ document.createRange = () => { return range; }; -describe('getNormalizedTypes', () => { - const typesAsString = 'Resource'; - it('should return the normalized types', () => { - const result = getNormalizedTypes(typesAsString); - expect(result).toEqual(['Resource']); - }); - - const typesAsUrl = 'https://bluebrain.github.io/nexus/vocabulary/Resource'; - it('should return the normalized types', () => { - const result = getNormalizedTypes(typesAsUrl); - expect(result).toEqual(['Resource']); - }); - const typesWithUrls = [ - 'https://bluebrain.github.io/nexus/vocabulary/Schema', - 'https://bluebrain.github.io/nexus/vocabulary/Resource', - 'https://bluebrain.github.io/nexus/vocabulary/Project', - 'Realm', - 'NeuronMorphology', - ]; - it('should return the normalized types', () => { - const result = getNormalizedTypes(typesWithUrls); - expect(result).toEqual([ - 'Schema', - 'Resource', - 'Project', - 'Realm', - 'NeuronMorphology', - ]); - }); -}); +const INVALID_URL = 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/invalid'; +const testFetcher: ResourceResolutionFetchFn = async ( + key, + { fetchContext } +) => { + const { resourceId } = fetchContext; + if (resourceId === INVALID_URL) { + return Promise.resolve({ + type: 'error', + data: new Error('Resource can not be resolved'), + }); + } + if (decodeURIComponent(resourceId) === resourceFromSearchApiId) { + return { + data: resourceFromSearchApi, + type: 'search-api', + }; + } + return { + data: new Error('Not found'), + type: 'error', + }; +}; describe('resolveLinkInEditor', () => { const queryClient = new QueryClient(); let store: Store; - let server: ReturnType; const defaultPaylaod = { top: 0, left: 0, open: true }; let nexus: NexusClient; let TestApp: JSX.Element; let component: RenderResult; let rerender: (ui: React.ReactElement) => void; - beforeAll(() => { - server = setupServer(getResolverResponseObject, getSearchApiResponseObject); - server.listen(); - }); - beforeEach(async () => { const history = createMemoryHistory({}); nexus = createNexusClient({ @@ -115,19 +100,16 @@ describe('resolveLinkInEditor', () => { afterEach(async () => { cleanup(); }); - afterAll(() => { - server.resetHandlers(); - server.close(); - }); // case-resource: can not be tested since codemirror issue and redirection // case-error: link can not be resolved by the project resolver nor the search api and it's not external it('should return null if the url is not valid', async () => { - const url = 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/invalid'; const { resolvedAs, error, results } = await editorLinkResolutionHandler({ nexus, - url, + url: INVALID_URL, + apiEndpoint: '/', orgLabel: 'orgLabel', projectLabel: 'projectLabel', + fetcher: testFetcher, }); store.dispatch({ type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, @@ -155,9 +137,11 @@ describe('resolveLinkInEditor', () => { await waitFor(async () => { const { resolvedAs, error, results } = await editorLinkResolutionHandler({ nexus, + apiEndpoint: '/', url: resourceFromSearchApiId, orgLabel: orgProject?.orgLabel, projectLabel: orgProject?.projectLabel, + fetcher: testFetcher, }); store.dispatch({ type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, @@ -193,35 +177,4 @@ describe('resolveLinkInEditor', () => { } }); }); - // case-external: link can not be resolved by the project resolver nor the search api and it's external - it('should show popover when external link is provided', async () => { - const url = 'ftp://www.google.com'; - await waitFor(async () => { - const { resolvedAs, error, results } = await editorLinkResolutionHandler({ - nexus, - url, - orgLabel: 'orgLabel', - projectLabel: 'projectLabel', - }); - store.dispatch({ - type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, - payload: { - ...defaultPaylaod, - resolvedAs, - error, - results, - }, - }); - rerender(TestApp); - expect( - store.getState().uiSettings.editorPopoverResolvedData.results._self - ).toEqual(url); - expect( - store.getState().uiSettings.editorPopoverResolvedData.resolvedAs - ).toEqual('external'); - expect( - store.getState().uiSettings.editorPopoverResolvedData.error - ).toBeUndefined(); - }); - }); }); From 9ac2f7aa3e0f8340ef3537aa5110bf8402d15fd4 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 11 Jul 2023 11:57:06 +0200 Subject: [PATCH 100/192] de-14/clean: remove popover components and logic from ui settings --- .../ResolvedLinkEditorPopover.tsx | 129 ------------------ .../ResolvedLinkEditorPopover/styles.less | 125 ----------------- .../useResolvedLinkEditorPopover.tsx | 102 -------------- src/shared/store/actions/ui-settings.ts | 7 +- .../store/reducers/__tests__/ui-settings.ts | 8 -- src/shared/store/reducers/ui-settings.ts | 40 +----- 6 files changed, 2 insertions(+), 409 deletions(-) delete mode 100644 src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx delete mode 100644 src/shared/molecules/ResolvedLinkEditorPopover/styles.less delete mode 100644 src/shared/molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover.tsx diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx deleted file mode 100644 index 1a8bcc165..000000000 --- a/src/shared/molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { ReactNode, useRef, forwardRef, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import { useMutation } from 'react-query'; -import { clsx } from 'clsx'; -import { Tag, notification } from 'antd'; -import * as Sentry from '@sentry/browser'; -import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; -import { RootState } from '../../store/reducers'; -import { TDELink } from '../../store/reducers/data-explorer'; -import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; -import useOnClickOutside from '../../hooks/useClickOutside'; -import useResolvedLinkEditorPopover from './useResolvedLinkEditorPopover'; -import './styles.less'; - -type TResultPattern = Pick; -type PopoverContainer = { - children: ReactNode; - onClickOutside(): void; -}; - -const DownloadResourceButton = ({ - downloadInProgress, - downloadBinaryAsync, -}: { - downloadInProgress: boolean; - downloadBinaryAsync: () => Promise; -}) => { - return ( -
- {downloadInProgress ? ( - - ) : ( - - )} -
- ); -}; -const PopoverContainer = forwardRef( - ({ children, onClickOutside }, ref) => { - const { - editorPopoverResolvedData: { top, left, resolvedAs }, - } = useSelector((state: RootState) => state.uiSettings); - - useOnClickOutside( - ref as React.MutableRefObject, - onClickOutside - ); - return ( -
- {children} -
- ); - } -); - -const ResolvedLinkEditorPopover = () => { - const ref = useRef(null); - const { - clickOutsideHandler, - navigateResourceHandler, - downloadBinaryAsyncHandler, - } = useResolvedLinkEditorPopover(); - - const { - editorPopoverResolvedData: { open, error, resolvedAs, results }, - } = useSelector((state: RootState) => state.uiSettings); - - const { - mutateAsync: downloadBinaryAsync, - isLoading: downloadInProgress, - } = useMutation({ - mutationKey: 'downloadBinary', - mutationFn: downloadBinaryAsyncHandler, - onError: error => { - notification.error({ - message: 'Download error', - description: 'Something went wrong while downloading the resource.', - }); - Sentry.captureException({ - error, - message: 'Download Binary error', - }); - }, - }); - - if (resolvedAs === 'resources' && open) { - return ( - - {(results as TDELink[]).map(item => ( -
- {item.resource?.[0] && item.resource?.[1] && ( - {`${item.resource?.[0]}/${item.resource?.[1]}`} - )} - - {item.isDownloadable && ( - - downloadBinaryAsync({ - orgLabel: item.resource?.[0]!, - projectLabel: item.resource?.[1]!, - resourceId: item.resource?.[2]!, - ext: item.resource?.[4] ?? 'json', - title: item.title, - }) - } - /> - )} -
- ))} -
- ); - } - return null; -}; - -export default ResolvedLinkEditorPopover; diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less b/src/shared/molecules/ResolvedLinkEditorPopover/styles.less deleted file mode 100644 index b1c10a7f1..000000000 --- a/src/shared/molecules/ResolvedLinkEditorPopover/styles.less +++ /dev/null @@ -1,125 +0,0 @@ -@import '../../lib.less'; - -.custom-popover-token { - position: absolute; - background: white; - box-shadow: 0px 2px 12px rgba(51, 51, 51, 0.12); - border-radius: 6px; - z-index: 9999; - max-width: 580px; - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - - .resource { - display: inline-flex; - align-items: center; - justify-content: flex-start; - gap: 2px; - width: 100%; - overflow: hidden; - span.ant-tag { - margin: 10px 12px; - border-radius: 4px; - box-shadow: 0 2px 12px rgba(#333, 0.12); - } - &.list-item:hover { - background-color: #efefef; - &:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - } - &:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - } - span.ant-tag { - transform: scale(1.04); - transition: transform 0.3s cubic-bezier(0.075, 0.82, 0.165, 1); - } - } - &.list-item { - padding: 2px 4px; - - span.ant-tag { - margin: 3px 12px; - } - .popover-btn.link { - padding: 3px 10px 3px 0; - } - } - .popover-btn.link { - border: none; - margin: 0; - padding: 0; - width: auto; - overflow: visible; - background: transparent; - cursor: pointer; - color: @fusion-daybreak-7; - white-space: pre-line; - text-overflow: ellipsis; - line-clamp: 1; - white-space: nowrap; - overflow: hidden; - width: fit-content; - padding: 10px 10px 10px 0; - } - } - - .popover-btn.error { - padding: 10px; - user-select: none; - } - - .external { - flex-direction: row; - align-items: center; - justify-content: center; - max-width: inherit; - gap: 10px; - padding: 10px; - span.ant-tag { - margin: 0; - } - .popover-btn.link { - padding: 0; - } - } - - &.error { - background-color: rgba(255, 0, 0, 0.478); - color: white; - span.ant-tag { - margin-right: 0px; - } - .popover-btn.error { - word-break: break-all; - } - } - - .popover-download-btn { - position: relative; - /* top: -0.06em; */ - display: inline-block; - height: auto; - align-self: stretch; - margin: 0 0 0 5px; - vertical-align: middle; - border-top: 0; - border-left: 1px solid rgba(0, 0, 0, 0.06); - display: flex; - align-items: center; - justify-content: center; - background-color: @fusion-main-color; - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; - padding: 4px 12px; - - svg { - font-size: 18px; - color: white; - } - } -} diff --git a/src/shared/molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover.tsx b/src/shared/molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover.tsx deleted file mode 100644 index 7808bfc1e..000000000 --- a/src/shared/molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useNexusContext } from '@bbp/react-nexus'; -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useLocation, useRouteMatch } from 'react-router'; -import { Resource } from '@bbp/nexus-sdk'; -import { RootState } from '../../store/reducers'; -import { UISettingsActionTypes } from '../../store/actions/ui-settings'; -import { - TDELink, - AddNewNodeDataExplorerGraphFlow, - InitNewVisitDataExplorerGraphView, -} from '../../store/reducers/data-explorer'; -import { editorPopoverResolvedDataInitialValue } from '../../store/reducers/ui-settings'; -import { - getNormalizedTypes, - getOrgAndProjectFromProjectId, - getResourceLabel, -} from '../../utils'; -import { parseResourceId } from '../../components/Preview/Preview'; -import { download } from '../../utils/download'; - -const useResolvedLinkEditorPopover = () => { - const nexus = useNexusContext(); - const dispatch = useDispatch(); - const navigate = useHistory(); - const routeMatch = useRouteMatch<{ - orgLabel: string; - projectLabel: string; - resourceId: string; - }>(`/:orgLabel/:projectLabel/resources/:resourceId`); - - const { pathname, search, state } = useLocation(); - - const clickOutsideHandler = () => { - dispatch({ - type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, - payload: editorPopoverResolvedDataInitialValue, - }); - }; - const navigateResourceHandler = async (resource: TDELink) => { - clickOutsideHandler(); - if (pathname === '/data-explorer/graph-flow') { - dispatch(AddNewNodeDataExplorerGraphFlow(resource)); - } else if (routeMatch?.url && routeMatch.params) { - const data = (await nexus.Resource.get( - routeMatch.params.orgLabel, - routeMatch.params.projectLabel, - routeMatch.params.resourceId - )) as Resource; - const orgProject = getOrgAndProjectFromProjectId(data._project); - dispatch( - InitNewVisitDataExplorerGraphView({ - referer: { pathname, search, state }, - source: { - _self: data._self, - title: getResourceLabel(data), - types: getNormalizedTypes(data['@type']), - resource: [ - orgProject?.orgLabel ?? '', - orgProject?.projectLabel ?? '', - data['@id'], - ], - }, - current: resource, - }) - ); - navigate.push('/data-explorer/graph-flow'); - } - }; - const downloadBinaryAsyncHandler = async ({ - orgLabel, - projectLabel, - resourceId, - ext, - title, - }: { - orgLabel: string; - projectLabel: string; - resourceId: string; - title: string; - ext?: string; - }) => { - try { - const data = await nexus.File.get( - orgLabel, - projectLabel, - encodeURIComponent(parseResourceId(resourceId)), - { as: 'blob' } - ); - return download(title, ext ?? 'json', data); - } catch (error) { - throw error; - } - }; - - return { - clickOutsideHandler, - navigateResourceHandler, - downloadBinaryAsyncHandler, - }; -}; - -export default useResolvedLinkEditorPopover; diff --git a/src/shared/store/actions/ui-settings.ts b/src/shared/store/actions/ui-settings.ts index 575734293..43204510f 100644 --- a/src/shared/store/actions/ui-settings.ts +++ b/src/shared/store/actions/ui-settings.ts @@ -1,5 +1,4 @@ -import { TEditorPopoverResolvedData } from '../reducers/ui-settings'; -import { FilterPayloadAction, PayloadAction } from './utils'; +import { FilterPayloadAction } from './utils'; export enum UISettingsActionTypes { CHANGE_PAGE_SIZE = 'CHANGE_PAGE_SIZE', @@ -13,8 +12,4 @@ type ChangePageSizeAction = FilterPayloadAction< UISettingsActionTypes.CHANGE_PAGE_SIZE, { pageSize: number } >; -export type TUpdateJSONEditorPopoverAction = PayloadAction< - UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, - TEditorPopoverResolvedData ->; export type UISettingsActions = ChangePageSizeAction; diff --git a/src/shared/store/reducers/__tests__/ui-settings.ts b/src/shared/store/reducers/__tests__/ui-settings.ts index a560662c4..56a9f0ca6 100644 --- a/src/shared/store/reducers/__tests__/ui-settings.ts +++ b/src/shared/store/reducers/__tests__/ui-settings.ts @@ -24,14 +24,6 @@ describe('UISettings Reducer', () => { orgsListPageSize: 50, }, currentResourceView: null, - editorPopoverResolvedData: { - error: null, - left: 0, - open: false, - resolvedAs: undefined, - results: [], - top: 0, - }, isAdvancedModeEnabled: false, }); }); diff --git a/src/shared/store/reducers/ui-settings.ts b/src/shared/store/reducers/ui-settings.ts index 407444d55..4ee338243 100644 --- a/src/shared/store/reducers/ui-settings.ts +++ b/src/shared/store/reducers/ui-settings.ts @@ -4,16 +4,7 @@ import { UISettingsActions, UISettingsActionTypes, } from '../actions/ui-settings'; -import { TDELink } from './data-explorer'; -export const editorPopoverResolvedDataInitialValue = { - top: 0, - left: 0, - open: false, - results: [], - error: null, - resolvedAs: undefined, -}; export const DEFAULT_UI_SETTINGS: UISettingsState = { openCreationPanel: false, pageSizes: { @@ -23,28 +14,13 @@ export const DEFAULT_UI_SETTINGS: UISettingsState = { linksListPageSize: 10, }, currentResourceView: null, - editorPopoverResolvedData: editorPopoverResolvedDataInitialValue, isAdvancedModeEnabled: false, }; -export type TEditorPopoverResolvedAs = - | 'resource' - | 'resources' - | 'external' - | 'error' - | undefined; -export type TEditorPopoverResolvedData = { - open: boolean; - top: number; - left: number; - results?: TDELink | TDELink[]; - resolvedAs: TEditorPopoverResolvedAs; - error?: any; -}; + export interface UISettingsState { openCreationPanel: boolean; pageSizes: { [key: string]: number }; currentResourceView: Resource | null; - editorPopoverResolvedData: TEditorPopoverResolvedData; isAdvancedModeEnabled: boolean; } @@ -73,20 +49,6 @@ export default function uiSettingsReducer( currentResourceView: action.payload, }; } - case UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER: { - return { - ...state, - editorPopoverResolvedData: { - ...state.editorPopoverResolvedData, - open: action.payload.open, - top: action.payload.top, - left: action.payload.left, - results: action.payload.results, - error: action.payload.error, - resolvedAs: action.payload.resolvedAs, - }, - }; - } case UISettingsActionTypes.ENABLE_ADVANCED_MODE: { return { ...state, From f2b5d93daa57f33ad8cbebc56ce3963b7ec62e96 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 11 Jul 2023 12:03:05 +0200 Subject: [PATCH 101/192] de-14/clean: remove test related to popover --- .../ResourceEditor/editorUtils.spec.tsx | 180 ------------------ 1 file changed, 180 deletions(-) delete mode 100644 src/shared/components/ResourceEditor/editorUtils.spec.tsx diff --git a/src/shared/components/ResourceEditor/editorUtils.spec.tsx b/src/shared/components/ResourceEditor/editorUtils.spec.tsx deleted file mode 100644 index cae3c1bf0..000000000 --- a/src/shared/components/ResourceEditor/editorUtils.spec.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import '@testing-library/jest-dom'; -import React from 'react'; -import { editorLinkResolutionHandler } from './editorUtils'; -import { Provider } from 'react-redux'; -import { createMemoryHistory } from 'history'; -import { NexusClient, createNexusClient } from '@bbp/nexus-sdk'; -import { Router } from 'react-router-dom'; -import { NexusProvider } from '@bbp/react-nexus'; -import { AnyAction, Store } from 'redux'; -import { QueryClientProvider, QueryClient } from 'react-query'; -import configureStore from '../../store'; -import { - resourceFromSearchApiId, - resourceFromSearchApi, -} from '../../../__mocks__/handlers/ResourceEditor/handlers'; -import { deltaPath } from '../../../__mocks__/handlers/handlers'; -import { getResourceLabel } from '../../utils'; -import { - render, - screen, - waitFor, - cleanup, - RenderResult, -} from '../../../utils/testUtil'; -import ResolvedLinkEditorPopover from '../../molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover'; -import { UISettingsActionTypes } from '../../store/actions/ui-settings'; -import { ResourceResolutionFetchFn } from './ResourcesLRUCache'; - -document.createRange = () => { - const range = new Range(); - - range.getBoundingClientRect = jest.fn(); - - range.getClientRects = () => { - return { - item: () => null, - length: 0, - [Symbol.iterator]: jest.fn(), - }; - }; - - return range; -}; - -const INVALID_URL = 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/invalid'; -const testFetcher: ResourceResolutionFetchFn = async ( - key, - { fetchContext } -) => { - const { resourceId } = fetchContext; - if (resourceId === INVALID_URL) { - return Promise.resolve({ - type: 'error', - data: new Error('Resource can not be resolved'), - }); - } - if (decodeURIComponent(resourceId) === resourceFromSearchApiId) { - return { - data: resourceFromSearchApi, - type: 'search-api', - }; - } - return { - data: new Error('Not found'), - type: 'error', - }; -}; - -describe('resolveLinkInEditor', () => { - const queryClient = new QueryClient(); - let store: Store; - const defaultPaylaod = { top: 0, left: 0, open: true }; - let nexus: NexusClient; - let TestApp: JSX.Element; - let component: RenderResult; - let rerender: (ui: React.ReactElement) => void; - - beforeEach(async () => { - const history = createMemoryHistory({}); - nexus = createNexusClient({ - fetch, - uri: deltaPath(), - }); - store = configureStore(history, { nexus }, {}); - TestApp = ( - - - - - - - - - - ); - component = render(TestApp); - rerender = component.rerender; - }); - - afterEach(async () => { - cleanup(); - }); - // case-resource: can not be tested since codemirror issue and redirection - // case-error: link can not be resolved by the project resolver nor the search api and it's not external - it('should return null if the url is not valid', async () => { - const { resolvedAs, error, results } = await editorLinkResolutionHandler({ - nexus, - url: INVALID_URL, - apiEndpoint: '/', - orgLabel: 'orgLabel', - projectLabel: 'projectLabel', - fetcher: testFetcher, - }); - store.dispatch({ - type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, - payload: { - ...defaultPaylaod, - resolvedAs, - error, - results, - }, - }); - rerender(TestApp); - expect( - store.getState().uiSettings.editorPopoverResolvedData.results - ).toBeUndefined(); - expect( - store.getState().uiSettings.editorPopoverResolvedData.resolvedAs - ).toEqual('error'); - }); - // case-resources: link can be resolved by search api and has multiple results - it('should show popover when the link is resolved by search api with multiple results', async () => { - const orgProject = { - orgLabel: 'bbp', - projectLabel: 'lnmce', - }; - await waitFor(async () => { - const { resolvedAs, error, results } = await editorLinkResolutionHandler({ - nexus, - apiEndpoint: '/', - url: resourceFromSearchApiId, - orgLabel: orgProject?.orgLabel, - projectLabel: orgProject?.projectLabel, - fetcher: testFetcher, - }); - store.dispatch({ - type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, - payload: { - ...defaultPaylaod, - resolvedAs, - error, - results, - }, - }); - rerender(TestApp); - expect( - store.getState().uiSettings.editorPopoverResolvedData.results - ).toBeDefined(); - expect( - store.getState().uiSettings.editorPopoverResolvedData.results.length - ).toEqual(resourceFromSearchApi._total); - expect( - store.getState().uiSettings.editorPopoverResolvedData.resolvedAs - ).toBe('resources'); - expect( - store.getState().uiSettings.editorPopoverResolvedData.error - ).toBeUndefined(); - for (const item in resourceFromSearchApi._results) { - const nameMatch = new RegExp( - getResourceLabel(resourceFromSearchApi._results[item]), - 'i' - ); - const namesInScreen = await screen.findAllByText(nameMatch); - for (const nameInScreen of namesInScreen) { - expect(nameInScreen).toBeInTheDocument(); - } - } - }); - }); -}); From 62854f06d15b33bfc0af7a3e4a31a294e2b742c2 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 11 Jul 2023 12:03:25 +0200 Subject: [PATCH 102/192] de-14/clean: remove popover form upper dome (APP) --- src/shared/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shared/App.tsx b/src/shared/App.tsx index 0d569b958..8f366665b 100644 --- a/src/shared/App.tsx +++ b/src/shared/App.tsx @@ -20,7 +20,7 @@ import CreateProject from './modals/CreateProject/CreateProject'; import CreateOrganization from './modals/CreateOrganization/CreateOrganization'; import CreateStudio from './modals/CreateStudio/CreateStudio'; import AppInfo from './modals/AppInfo/AppInfo'; -import ResolvedLinkEditorPopover from './molecules/ResolvedLinkEditorPopover/ResolvedLinkEditorPopover'; + import './App.less'; const App: React.FC = () => { @@ -53,7 +53,6 @@ const App: React.FC = () => { - {userAuthenticated && ( From 88f396357b7fd5a3982b5c2a52b5ce29e1d2554d Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 11 Jul 2023 12:05:15 +0200 Subject: [PATCH 103/192] de-14/update: add event listner for popover and add the logic --- .../components/ResourceEditor/CodeEditor.tsx | 6 - .../ResourceEditor/ResourceEditor.less | 32 +- .../ResourceEditor/ResourceEditor.spec.tsx | 2 - .../components/ResourceEditor/editorUtils.ts | 67 +- .../components/ResourceEditor/index.tsx | 57 +- .../ResourceEditor/useEditorTooltip.tsx | 577 +++++++++++------- 6 files changed, 422 insertions(+), 319 deletions(-) diff --git a/src/shared/components/ResourceEditor/CodeEditor.tsx b/src/shared/components/ResourceEditor/CodeEditor.tsx index 7a4c24c8c..7886a79a4 100644 --- a/src/shared/components/ResourceEditor/CodeEditor.tsx +++ b/src/shared/components/ResourceEditor/CodeEditor.tsx @@ -9,11 +9,9 @@ type TCodeEditor = { busy: boolean; value: string; editable: boolean; - loadingResolution: boolean; fullscreen: boolean; keyFoldCode(cm: any): void; handleChange(editor: any, data: any, value: any): void; - onLinkClick(_: any, ev: MouseEvent): void; onLinksFound(): void; }; type TEditorConfiguration = EditorConfiguration & { @@ -28,9 +26,7 @@ const CodeEditor = forwardRef( editable, fullscreen, keyFoldCode, - loadingResolution, handleChange, - onLinkClick, onLinksFound, }, ref @@ -61,14 +57,12 @@ const CodeEditor = forwardRef( } className={clsx( 'code-mirror-editor', - loadingResolution && 'resolution-on-progress', fullscreen && 'full-screen-mode' )} onChange={handleChange} editorDidMount={editor => { (ref as React.MutableRefObject).current = editor; }} - onMouseDown={onLinkClick} onUpdate={onLinksFound} /> diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 4d85d8b4e..a7faf3755 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -102,7 +102,7 @@ } } } - +.CodeMirror-hover-tooltip-popover, .CodeMirror-hover-tooltip { background-color: white; border: 1px solid 333; @@ -116,7 +116,33 @@ white-space: pre-wrap; transition: all 0.4s ease-in-out; padding: 4px 0; - + &.popover { + background-color: white !important; + .CodeMirror-hover-tooltip-resources-content { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + .CodeMirror-hover-tooltip-item { + width: 100%; + padding: 4px; + align-items: center; + justify-content: flex-start; + cursor: pointer; + .tag { + background-color: @fusion-primary-color; + } + &:hover { + background-color: @fusion-blue-0; + color: @fusion-primary-color; + .tag { + background-color: white; + color: @fusion-primary-color; + } + } + } + } + } &-content { display: flex; flex-direction: column; @@ -156,7 +182,7 @@ justify-content: center; padding: 2px 10px 2px 5px; overflow: hidden; - + user-select: none; .tag { color: white; padding: 2px 5px; diff --git a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx index 6d913fd89..f33982c32 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx +++ b/src/shared/components/ResourceEditor/ResourceEditor.spec.tsx @@ -30,12 +30,10 @@ describe('ResourceEditor', () => { data-testid="code-mirror-editor" value={JSON.stringify(resourceResolverApi)} editable={false} - onLinkClick={() => {}} onLinksFound={onLinksFound} busy={false} keyFoldCode={() => {}} handleChange={() => {}} - loadingResolution={false} ref={editor} fullscreen={false} /> diff --git a/src/shared/components/ResourceEditor/editorUtils.ts b/src/shared/components/ResourceEditor/editorUtils.ts index fe3ace48c..4a4d519bb 100644 --- a/src/shared/components/ResourceEditor/editorUtils.ts +++ b/src/shared/components/ResourceEditor/editorUtils.ts @@ -1,28 +1,37 @@ import { NexusClient, Resource } from '@bbp/nexus-sdk'; import { has } from 'lodash'; -import { useDispatch } from 'react-redux'; -import useResolvedLinkEditorPopover from '../../molecules/ResolvedLinkEditorPopover/useResolvedLinkEditorPopover'; import isValidUrl, { isExternalLink, isStorageLink, isUrlCurieFormat, } from '../../../utils/validUrl'; -import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; import { getNormalizedTypes, getOrgAndProjectFromResourceObject, getResourceLabel, } from '../../utils'; import { TDELink, TDEResource } from '../../store/reducers/data-explorer'; -import { UISettingsActionTypes } from '../../store/actions/ui-settings'; import ResourceResolutionCache, { ResourceResolutionFetchFn, TPagedResources, TResolutionData, - TResolutionReturnedData, TResolutionType, } from './ResourcesLRUCache'; +export type TEditorPopoverResolvedAs = + | 'resource' + | 'resources' + | 'external' + | 'error' + | undefined; +export type TEditorPopoverResolvedData = { + open: boolean; + top: number; + left: number; + results?: TDELink | TDELink[]; + resolvedAs: TEditorPopoverResolvedAs; + error?: any; +}; type TDeltaError = Error & { '@type': string; details: string; @@ -226,51 +235,3 @@ export async function editorLinkResolutionHandler({ } } } - -export const useResourceResoultion = () => { - const dispatch = useDispatch(); - const { - navigateResourceHandler, - downloadBinaryAsyncHandler, - } = useResolvedLinkEditorPopover(); - - return ({ - resolvedAs, - error, - results, - top, - left, - }: TEditorPopoverResolvedData) => { - if (resolvedAs === 'resource') { - 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 navigateResourceHandler(result); - } - if (resolvedAs === 'external') { - return window.open( - (results as TDELink)._self, - '_blank', - 'noopener noreferrer' - ); - } - return dispatch({ - type: UISettingsActionTypes.UPDATE_JSON_EDITOR_POPOVER, - payload: { - top, - left, - resolvedAs, - error, - results, - open: true, - }, - }); - }; -}; diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index cb93d64f5..1849217c8 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -20,14 +20,12 @@ import isValidUrl, { isUrlCurieFormat, } from '../../../utils/validUrl'; import CodeEditor from './CodeEditor'; -import { - mayBeResolvableLink, - useResourceResoultion, - editorLinkResolutionHandler, - getTokenAndPosAt, -} from './editorUtils'; import { RootState } from '../../store/reducers'; -import useEditorTooltip, { CODEMIRROR_LINK_CLASS } from './useEditorTooltip'; +import { + useEditorPopover, + useEditorTooltip, + CODEMIRROR_LINK_CLASS, +} from './useEditorTooltip'; import { DATA_EXPLORER_GRAPH_FLOW_PATH } from '../../store/reducers/data-explorer'; import ResourceResolutionCache from './ResourcesLRUCache'; @@ -77,11 +75,7 @@ const ResourceEditor: React.FunctionComponent = props => { onFullScreen, showControlPanel = true, } = props; - - const nexus = useNexusContext(); const location = useLocation(); - const onResolutionComplete = useResourceResoultion(); - const [loadingResolution, setLoadingResolution] = React.useState(false); const [isEditing, setEditing] = React.useState(editing); const [valid, setValid] = React.useState(true); const [parsedValue, setParsedValue] = React.useState(rawData); @@ -143,40 +137,6 @@ const ResourceEditor: React.FunctionComponent = props => { }); }; - const onLinkClick = async (_: any, ev: MouseEvent) => { - if (codeMirorRef.current) { - try { - setLoadingResolution(true); - const { coords, token } = getTokenAndPosAt(ev, codeMirorRef.current); - const url = token?.string.replace(/\\/g, '').replace(/\"/g, ''); - if (url && mayBeResolvableLink(url)) { - ev.stopPropagation(); - const { - resolvedAs, - results, - error, - } = await editorLinkResolutionHandler({ - nexus, - apiEndpoint, - url, - orgLabel, - projectLabel, - }); - onResolutionComplete({ - ...coords, - resolvedAs, - results, - error, - open: true, - }); - } - } catch (error) { - } finally { - setLoadingResolution(false); - } - } - }; - React.useEffect(() => { setEditing(false); setStringValue(JSON.stringify(rawData, null, 2)); // Update copy of the rawData for the editor. @@ -221,6 +181,11 @@ const ResourceEditor: React.FunctionComponent = props => { isEditing, ref: codeMirorRef, }); + useEditorPopover({ + orgLabel, + projectLabel, + ref: codeMirorRef, + }); React.useEffect(() => { return () => { @@ -312,8 +277,6 @@ const ResourceEditor: React.FunctionComponent = props => { editable={editable} handleChange={handleChange} keyFoldCode={keyFoldCode} - loadingResolution={loadingResolution} - onLinkClick={onLinkClick} onLinksFound={onLinksFound} fullscreen={limited} /> diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index ba6ad851d..b82673536 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -4,240 +4,401 @@ import clsx from 'clsx'; import { useNexusContext } from '@bbp/react-nexus'; import { useSelector } from 'react-redux'; import { - editorLinkResolutionHandler, - getTokenAndPosAt, - mayBeResolvableLink, + TEditorPopoverResolvedData, + editorLinkResolutionHandler, + getTokenAndPosAt, + mayBeResolvableLink, } from './editorUtils'; import { TDELink } from '../../store/reducers/data-explorer'; -import { TEditorPopoverResolvedData } from '../../store/reducers/ui-settings'; import { RootState } from '../../store/reducers'; +import useResolutionActions from './useResolutionActions'; const downloadImg = require('../../images/DownloadingLoop.svg'); + export const CODEMIRROR_HOVER_CLASS = 'CodeMirror-hover-tooltip'; export const CODEMIRROR_LINK_CLASS = 'fusion-resource-link'; +type TTooltipCreator = Pick< + TEditorPopoverResolvedData, + 'error' | 'resolvedAs' | 'results' +>; + +function removePopoversFromDOM() { + const popovers = document.querySelectorAll( + `.${CODEMIRROR_HOVER_CLASS}-popover` + ); + popovers.forEach(popover => popover.remove()); +} +function removeTooltipsFromDOM() { + const tooltips = document.getElementsByClassName(CODEMIRROR_HOVER_CLASS); + tooltips && + Array.from(tooltips).forEach(tooltip => { + tooltip.remove(); + }); +} function createTooltipNode({ - tag, - title, - isDownloadable, + tag, + title, + isDownloadable, }: { - tag: string | null; - title: string; - isDownloadable?: boolean; + tag: string | null; + title: string; + isDownloadable?: boolean; }) { - const tooltipItemContent = document.createElement('div'); - tooltipItemContent.className = 'CodeMirror-hover-tooltip-item'; - const nodeTag = document.createElement('div'); - nodeTag.className = 'tag'; - tag && nodeTag.appendChild(document.createTextNode(tag)); - tooltipItemContent.appendChild(nodeTag); - - const nodeTitle = document.createElement('span'); - nodeTitle.className = 'title'; - nodeTitle.appendChild(document.createTextNode(title)); - tooltipItemContent.appendChild(nodeTitle); - - const nodeDownload = isDownloadable && document.createElement('img'); - nodeDownload && nodeDownload.setAttribute('src', downloadImg); - nodeDownload && nodeDownload.classList.add('download-icon'); - nodeDownload && tooltipItemContent.appendChild(nodeDownload); - - return tooltipItemContent; + const tooltipItemContent = document.createElement('div'); + tooltipItemContent.className = 'CodeMirror-hover-tooltip-item'; + const nodeTag = document.createElement('div'); + nodeTag.className = 'tag'; + tag && nodeTag.appendChild(document.createTextNode(tag)); + tooltipItemContent.appendChild(nodeTag); + + const nodeTitle = document.createElement('span'); + nodeTitle.className = 'title'; + nodeTitle.appendChild(document.createTextNode(title)); + tooltipItemContent.appendChild(nodeTitle); + + const nodeDownload = isDownloadable && document.createElement('img'); + nodeDownload && nodeDownload.setAttribute('src', downloadImg); + nodeDownload && nodeDownload.classList.add('download-icon'); + nodeDownload && tooltipItemContent.appendChild(nodeDownload); + + return tooltipItemContent; } -function createTooltipContent({ - resolvedAs, - error, - results, -}: Pick) { - const tooltipContent = document.createElement('div'); - tooltipContent.className = clsx( - `${CODEMIRROR_HOVER_CLASS}-content`, - resolvedAs && resolvedAs - ); - if (resolvedAs === 'error' && error) { - tooltipContent.appendChild( - createTooltipNode({ - tag: 'Error', - title: error, - }) - ); - return tooltipContent; - } - if (resolvedAs === 'resource') { - const result = results as TDELink; - tooltipContent.appendChild( - createTooltipNode({ - tag: result.resource - ? `${result.resource?.[0]}/${result.resource?.[1]}` - : null, - title: result.title ?? result._self, - isDownloadable: result.isDownloadable, - }) - ); - return tooltipContent; - } - if (resolvedAs === 'resources') { - tooltipContent.appendChild( - createTooltipNode({ - tag: 'Multiple', - title: `${ - (results as TDELink[]).length - } resources was found, click to list them`, - }) +function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { + const tooltipContent = document.createElement('div'); + tooltipContent.className = clsx( + `${CODEMIRROR_HOVER_CLASS}-content`, + resolvedAs && resolvedAs ); - return tooltipContent; - } - if (resolvedAs === 'external') { - tooltipContent.appendChild( - createTooltipNode({ - tag: 'External', - title: (results as TDELink).title ?? (results as TDELink)._self, - }) + if (resolvedAs === 'error' && error) { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'Error', + title: error, + }) + ); + return tooltipContent; + } + if (resolvedAs === 'resource') { + const result = results as TDELink; + tooltipContent.appendChild( + createTooltipNode({ + tag: result.resource + ? `${result.resource?.[0]}/${result.resource?.[1]}` + : null, + title: result.title ?? result._self, + isDownloadable: result.isDownloadable, + }) + ); + return tooltipContent; + } + if (resolvedAs === 'resources') { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'Multiple', + title: `${(results as TDELink[]).length + } resources was found, click to list them`, + }) + ); + return tooltipContent; + } + if (resolvedAs === 'external') { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'External', + title: (results as TDELink).title ?? (results as TDELink)._self, + }) + ); + return tooltipContent; + } + return null; +} + +function createPopoverContent({ + results, + onClick, +}: { + results: TDELink[]; + onClick: (result: TDELink) => void; +}) { + const tooltipContent = document.createElement('div'); + tooltipContent.className = clsx( + `${CODEMIRROR_HOVER_CLASS}-resources-content` ); + // create node for each link in results and then append it to the tooltipContent + (results as TDELink[]).forEach((link: TDELink) => { + const linkNode = createTooltipNode({ + tag: link.resource ? `${link.resource?.[0]}/${link.resource?.[1]}` : null, + title: link.title ?? link._self, + isDownloadable: link.isDownloadable, + }); + linkNode.onclick = () => { + removePopoversFromDOM(); + onClick(link); + }; + return tooltipContent.appendChild(linkNode); + }); return tooltipContent; - } - return null; } - function useEditorTooltip({ - ref, - isEditing, - orgLabel, - projectLabel, + ref, + isEditing, + orgLabel, + projectLabel, }: { - ref: React.MutableRefObject; - isEditing: boolean; - orgLabel: string; - projectLabel: string; + ref: React.MutableRefObject; + isEditing: boolean; + orgLabel: string; + projectLabel: string; }) { - const nexus = useNexusContext(); - const { - config: { apiEndpoint }, - ui: { - editorPopoverResolvedData: { open: isPopoverOpen }, - }, - } = useSelector((state: RootState) => ({ - dataExplorer: state.dataExplorer, - config: state.config, - ui: state.uiSettings, - })); - - const allowTooltip = !isEditing && !isPopoverOpen; - - React.useEffect(() => { - const currentEditor = (ref as React.MutableRefObject) - ?.current; - const editorWrapper = currentEditor.getWrapperElement(); - function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { - const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); - const tooltipRect = tooltip.getBoundingClientRect(); - if (tooltipRect.height <= editorRect.top) { - tooltip.style.top = `${editorRect.top - tooltipRect.height}px`; - } else { - tooltip.style.top = `${editorRect.bottom}px`; - } - tooltip.style.left = `${editorRect.left}px`; - } - function removeTooltipFromDom(tooltip: HTMLDivElement) { - if (tooltip.parentNode) { - tooltip.parentNode.removeChild(tooltip); - } - } - function hideTooltip(tooltip: HTMLDivElement) { - if (!tooltip.parentNode) { - return; - } - setTimeout(() => { - removeTooltipFromDom(tooltip); - }, 300); - } - function showTooltip(content: HTMLDivElement, node: HTMLElement) { - const tooltip = document.createElement('div'); - tooltip.className = CODEMIRROR_HOVER_CLASS; - tooltip.appendChild(content); - document.body.appendChild(tooltip); - - function hide() { - if (tooltip) { - node.classList.remove('has-tooltip'); - hideTooltip(tooltip); - tooltip.remove(); + const nexus = useNexusContext(); + const { + config: { apiEndpoint }, + } = useSelector((state: RootState) => ({ + config: state.config, + })); + + const allowTooltip = !isEditing; + + React.useEffect(() => { + const currentEditor = (ref as React.MutableRefObject) + ?.current; + const editorWrapper = currentEditor.getWrapperElement(); + function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { + const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + if (tooltipRect.height <= editorRect.top) { + tooltip.style.top = `${editorRect.top - tooltipRect.height}px`; + } else { + tooltip.style.top = `${editorRect.bottom}px`; + } + tooltip.style.left = `${editorRect.left}px`; + } + function removeTooltipFromDom(tooltip: HTMLDivElement) { + if (tooltip.parentNode) { + tooltip.parentNode.removeChild(tooltip); + } } - node.removeEventListener('mouseout', hide); - node.removeEventListener('click', hide); - } + function hideTooltip(tooltip: HTMLDivElement) { + if (!tooltip.parentNode) { + return; + } + setTimeout(() => { + removeTooltipFromDom(tooltip); + }, 300); + } + function showTooltip(content: HTMLDivElement, node: HTMLElement) { + const tooltip = document.createElement('div'); + tooltip.className = CODEMIRROR_HOVER_CLASS; + tooltip.appendChild(content); + document.body.appendChild(tooltip); + + function hide() { + if (tooltip) { + node.classList.remove('has-tooltip'); + hideTooltip(tooltip); + tooltip.remove(); + } + node.removeEventListener('mouseout', hide); + node.removeEventListener('click', hide); + node.removeEventListener('scroll', hide); + } + + node.addEventListener('mouseout', hide); + node.addEventListener('click', hide); + node.addEventListener('scroll', hide); - node.addEventListener('mouseout', hide); - node.addEventListener('click', hide); + const pool: ReturnType = setTimeout(() => { + if (tooltip) { + hideTooltip(tooltip); + tooltip.remove(); + } + return clearTimeout(pool); + }, 3000); - const pool: ReturnType = setTimeout(() => { - if (tooltip) { - hideTooltip(tooltip); - tooltip.remove(); + return tooltip; } - return clearTimeout(pool); - }, 3000); - return tooltip; - } + async function onMouseOver(ev: MouseEvent) { + const node = ev.target as HTMLElement; + if (node) { + const { token } = getTokenAndPosAt(ev, currentEditor); + const content = token?.string || ''; + const url = content.replace(/\\/g, '').replace(/\"/g, ''); + if (url && mayBeResolvableLink(url)) { + node.classList.add('wait-for-tooltip'); + removeTooltipsFromDOM(); + editorLinkResolutionHandler({ + nexus, + apiEndpoint, + url, + orgLabel, + projectLabel, + }).then(({ resolvedAs, results, error }) => { + const tooltipContent = createTooltipContent({ + resolvedAs, + error, + results, + }); + if (tooltipContent) { + node.classList.remove('wait-for-tooltip'); + node.classList.add( + resolvedAs === 'error' + ? 'error' + : resolvedAs === 'resource' && + (results as TDELink).isDownloadable + ? 'downloadable' + : 'has-tooltip' + ); + const tooltip = showTooltip(tooltipContent, node); + const calculatePosition = (ev: MouseEvent) => + positionner(ev, tooltip); + editorWrapper.addEventListener('mousemove', calculatePosition); + } + }); + } + } + } + // allow the tooltip only when the editor is not in edition mode + // and the popover is not open + allowTooltip && editorWrapper.addEventListener('mouseover', onMouseOver); + // remove the event listener when not allwoed + !allowTooltip && + editorWrapper.removeEventListener('mouseover', onMouseOver); + + // cleanup + // remove the event listener when the component is unmounted + return () => { + allowTooltip && + editorWrapper.removeEventListener('mouseover', onMouseOver); + }; + }, [ + (ref as React.MutableRefObject)?.current, + allowTooltip, + ]); +} +function useEditorPopover({ + ref, + orgLabel, + projectLabel, +}: { + ref: React.MutableRefObject; + orgLabel: string; + projectLabel: string; +}) { + const nexus = useNexusContext(); + const { + navigateResourceHandler, + downloadBinaryAsyncHandler, + } = useResolutionActions(); + const { + config: { apiEndpoint }, + } = useSelector((state: RootState) => ({ + config: state.config, + })); - async function onMouseOver(ev: MouseEvent) { - const node = ev.target as HTMLElement; - if (node) { - const { token } = getTokenAndPosAt(ev, currentEditor); - const content = token?.string || ''; - const url = content.replace(/\\/g, '').replace(/\"/g, ''); - if (url && mayBeResolvableLink(url)) { - node.classList.add('wait-for-tooltip'); - const tooltips = document.getElementsByClassName( - CODEMIRROR_HOVER_CLASS - ); - tooltips && - Array.from(tooltips).forEach(tooltip => { - tooltip.remove(); - }); - editorLinkResolutionHandler({ - nexus, - apiEndpoint, - url, - orgLabel, - projectLabel, - }).then(({ resolvedAs, results, error }) => { - const tooltipContent = createTooltipContent({ - resolvedAs, - error, - results, - }); - if (tooltipContent) { - node.classList.remove('wait-for-tooltip'); - node.classList.add( - resolvedAs === 'error' - ? 'error' - : resolvedAs === 'resource' && - (results as TDELink).isDownloadable - ? 'downloadable' - : 'has-tooltip' - ); - const tooltip = showTooltip(tooltipContent, node); - const calculatePosition = (ev: MouseEvent) => - positionner(ev, tooltip); - editorWrapper.addEventListener('mousemove', calculatePosition); + React.useEffect(() => { + const currentEditor = (ref as React.MutableRefObject) + ?.current; + const editorWrapper = currentEditor.getWrapperElement(); + function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { + const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + if (tooltipRect.height <= editorRect.top) { + tooltip.style.top = `${editorRect.top - tooltipRect.height}px`; + } else { + tooltip.style.top = `${editorRect.bottom}px`; } - }); + tooltip.style.left = `${editorRect.left}px`; } - } - } - allowTooltip && editorWrapper.addEventListener('mouseover', onMouseOver); - !allowTooltip && - editorWrapper.removeEventListener('mouseover', onMouseOver); - return () => { - allowTooltip && - editorWrapper.removeEventListener('mouseover', onMouseOver); - }; - }, [ - (ref as React.MutableRefObject)?.current, - allowTooltip, - ]); + function showTooltip(content: HTMLDivElement, node: HTMLElement) { + const tooltip = document.createElement('div'); + tooltip.className = `${CODEMIRROR_HOVER_CLASS}-popover popover`; + tooltip.appendChild(content); + document.body.appendChild(tooltip); + return tooltip; + } + function onEditorMouseDown(ev: MouseEvent, node: HTMLElement) { + if ( + ev.target && + !node.contains(ev.target as HTMLElement) && + !(ev.target as HTMLElement).isEqualNode(node) && + (ev.target as HTMLElement).closest('.CodeMirror-wrap') + ) { + removePopoversFromDOM(); + } + editorWrapper.removeEventListener('mousedown', (ev: MouseEvent) => + onEditorMouseDown(ev, node) + ); + } + async function onMouseDown(_: CodeMirror.Editor, ev: MouseEvent) { + const node = ev.target as HTMLElement; + if (node) { + const { token } = getTokenAndPosAt(ev, currentEditor); + const content = token?.string || ''; + const url = content.replace(/\\/g, '').replace(/\"/g, ''); + if (url && mayBeResolvableLink(url)) { + editorLinkResolutionHandler({ + nexus, + apiEndpoint, + url, + orgLabel, + projectLabel, + }).then(({ resolvedAs, results }) => { + switch (resolvedAs) { + case 'resources': { + const tooltipContent = createPopoverContent({ + results: results as TDELink[], + onClick: navigateResourceHandler, + }); + if (tooltipContent) { + const tooltip = showTooltip(tooltipContent, node); + positionner(ev, tooltip); + editorWrapper.addEventListener( + 'mousedown', + (ev: MouseEvent) => onEditorMouseDown(ev, node) + ); + } + break; + } + case 'resource': { + 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 navigateResourceHandler(result); + } + case 'external': { + window.open( + (results as TDELink)._self, + '_blank', + 'noopener noreferrer' + ); + break; + } + case 'error': + default: + break; + } + return; + }); + } + } + } + currentEditor.on('mousedown', onMouseDown); + return () => { + currentEditor.off('mousedown', onMouseDown); + }; + }, [ + (ref as React.MutableRefObject)?.current, + navigateResourceHandler, + ]); } -export default useEditorTooltip; +export { useEditorPopover, useEditorTooltip }; From 618e20287f302c1d907c51bb3b82e3a53d01cf58 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 11 Jul 2023 12:05:37 +0200 Subject: [PATCH 104/192] de-14/refactor: resolution functions --- .../ResourceEditor/useResolutionActions.tsx | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/shared/components/ResourceEditor/useResolutionActions.tsx diff --git a/src/shared/components/ResourceEditor/useResolutionActions.tsx b/src/shared/components/ResourceEditor/useResolutionActions.tsx new file mode 100644 index 000000000..5f1772589 --- /dev/null +++ b/src/shared/components/ResourceEditor/useResolutionActions.tsx @@ -0,0 +1,91 @@ +import { useNexusContext } from '@bbp/react-nexus'; +import { useDispatch } from 'react-redux'; +import { useHistory, useLocation, useRouteMatch } from 'react-router'; +import { Resource } from '@bbp/nexus-sdk'; +import { + TDELink, + AddNewNodeDataExplorerGraphFlow, + InitNewVisitDataExplorerGraphView, +} from '../../store/reducers/data-explorer'; +import { + getNormalizedTypes, + getOrgAndProjectFromProjectId, + getResourceLabel, +} from '../../utils'; +import { parseResourceId } from '../Preview/Preview'; +import { download } from '../../utils/download'; + +const useResolvedLinkEditorPopover = () => { + const nexus = useNexusContext(); + const dispatch = useDispatch(); + const navigate = useHistory(); + const routeMatch = useRouteMatch<{ + orgLabel: string; + projectLabel: string; + resourceId: string; + }>(`/:orgLabel/:projectLabel/resources/:resourceId`); + + const { pathname, search, state } = useLocation(); + + const navigateResourceHandler = async (resource: TDELink) => { + if (pathname === '/data-explorer/graph-flow') { + dispatch(AddNewNodeDataExplorerGraphFlow(resource)); + } else if (routeMatch?.url && routeMatch.params) { + const data = (await nexus.Resource.get( + routeMatch.params.orgLabel, + routeMatch.params.projectLabel, + routeMatch.params.resourceId + )) as Resource; + const orgProject = getOrgAndProjectFromProjectId(data._project); + dispatch( + InitNewVisitDataExplorerGraphView({ + referer: { pathname, search, state }, + source: { + _self: data._self, + title: getResourceLabel(data), + types: getNormalizedTypes(data['@type']), + resource: [ + orgProject?.orgLabel ?? '', + orgProject?.projectLabel ?? '', + data['@id'], + ], + }, + current: resource, + }) + ); + navigate.push('/data-explorer/graph-flow'); + } + }; + const downloadBinaryAsyncHandler = async ({ + orgLabel, + projectLabel, + resourceId, + ext, + title, + }: { + orgLabel: string; + projectLabel: string; + resourceId: string; + title: string; + ext?: string; + }) => { + try { + const data = await nexus.File.get( + orgLabel, + projectLabel, + encodeURIComponent(parseResourceId(resourceId)), + { as: 'blob' } + ); + return download(title, ext ?? 'json', data); + } catch (error) { + throw error; + } + }; + + return { + navigateResourceHandler, + downloadBinaryAsyncHandler, + }; +}; + +export default useResolvedLinkEditorPopover; From 63e393861f17e42ed8794f2dbc3d8cd5ed53b46d Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Tue, 11 Jul 2023 12:06:26 +0200 Subject: [PATCH 105/192] de-14/clean: fix styles format --- .../ResourceEditor/useEditorTooltip.tsx | 683 +++++++++--------- 1 file changed, 342 insertions(+), 341 deletions(-) diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index b82673536..d8d5f6fc4 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -4,10 +4,10 @@ import clsx from 'clsx'; import { useNexusContext } from '@bbp/react-nexus'; import { useSelector } from 'react-redux'; import { - TEditorPopoverResolvedData, - editorLinkResolutionHandler, - getTokenAndPosAt, - mayBeResolvableLink, + TEditorPopoverResolvedData, + editorLinkResolutionHandler, + getTokenAndPosAt, + mayBeResolvableLink, } from './editorUtils'; import { TDELink } from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; @@ -18,387 +18,388 @@ const downloadImg = require('../../images/DownloadingLoop.svg'); export const CODEMIRROR_HOVER_CLASS = 'CodeMirror-hover-tooltip'; export const CODEMIRROR_LINK_CLASS = 'fusion-resource-link'; type TTooltipCreator = Pick< - TEditorPopoverResolvedData, - 'error' | 'resolvedAs' | 'results' + TEditorPopoverResolvedData, + 'error' | 'resolvedAs' | 'results' >; function removePopoversFromDOM() { - const popovers = document.querySelectorAll( - `.${CODEMIRROR_HOVER_CLASS}-popover` - ); - popovers.forEach(popover => popover.remove()); + const popovers = document.querySelectorAll( + `.${CODEMIRROR_HOVER_CLASS}-popover` + ); + popovers.forEach(popover => popover.remove()); } function removeTooltipsFromDOM() { - const tooltips = document.getElementsByClassName(CODEMIRROR_HOVER_CLASS); - tooltips && - Array.from(tooltips).forEach(tooltip => { - tooltip.remove(); - }); + const tooltips = document.getElementsByClassName(CODEMIRROR_HOVER_CLASS); + tooltips && + Array.from(tooltips).forEach(tooltip => { + tooltip.remove(); + }); } function createTooltipNode({ - tag, - title, - isDownloadable, + tag, + title, + isDownloadable, }: { - tag: string | null; - title: string; - isDownloadable?: boolean; + tag: string | null; + title: string; + isDownloadable?: boolean; }) { - const tooltipItemContent = document.createElement('div'); - tooltipItemContent.className = 'CodeMirror-hover-tooltip-item'; - const nodeTag = document.createElement('div'); - nodeTag.className = 'tag'; - tag && nodeTag.appendChild(document.createTextNode(tag)); - tooltipItemContent.appendChild(nodeTag); + const tooltipItemContent = document.createElement('div'); + tooltipItemContent.className = 'CodeMirror-hover-tooltip-item'; + const nodeTag = document.createElement('div'); + nodeTag.className = 'tag'; + tag && nodeTag.appendChild(document.createTextNode(tag)); + tooltipItemContent.appendChild(nodeTag); - const nodeTitle = document.createElement('span'); - nodeTitle.className = 'title'; - nodeTitle.appendChild(document.createTextNode(title)); - tooltipItemContent.appendChild(nodeTitle); + const nodeTitle = document.createElement('span'); + nodeTitle.className = 'title'; + nodeTitle.appendChild(document.createTextNode(title)); + tooltipItemContent.appendChild(nodeTitle); - const nodeDownload = isDownloadable && document.createElement('img'); - nodeDownload && nodeDownload.setAttribute('src', downloadImg); - nodeDownload && nodeDownload.classList.add('download-icon'); - nodeDownload && tooltipItemContent.appendChild(nodeDownload); + const nodeDownload = isDownloadable && document.createElement('img'); + nodeDownload && nodeDownload.setAttribute('src', downloadImg); + nodeDownload && nodeDownload.classList.add('download-icon'); + nodeDownload && tooltipItemContent.appendChild(nodeDownload); - return tooltipItemContent; + return tooltipItemContent; } function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { - const tooltipContent = document.createElement('div'); - tooltipContent.className = clsx( - `${CODEMIRROR_HOVER_CLASS}-content`, - resolvedAs && resolvedAs + const tooltipContent = document.createElement('div'); + tooltipContent.className = clsx( + `${CODEMIRROR_HOVER_CLASS}-content`, + resolvedAs && resolvedAs + ); + if (resolvedAs === 'error' && error) { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'Error', + title: error, + }) ); - if (resolvedAs === 'error' && error) { - tooltipContent.appendChild( - createTooltipNode({ - tag: 'Error', - title: error, - }) - ); - return tooltipContent; - } - if (resolvedAs === 'resource') { - const result = results as TDELink; - tooltipContent.appendChild( - createTooltipNode({ - tag: result.resource - ? `${result.resource?.[0]}/${result.resource?.[1]}` - : null, - title: result.title ?? result._self, - isDownloadable: result.isDownloadable, - }) - ); - return tooltipContent; - } - if (resolvedAs === 'resources') { - tooltipContent.appendChild( - createTooltipNode({ - tag: 'Multiple', - title: `${(results as TDELink[]).length - } resources was found, click to list them`, - }) - ); - return tooltipContent; - } - if (resolvedAs === 'external') { - tooltipContent.appendChild( - createTooltipNode({ - tag: 'External', - title: (results as TDELink).title ?? (results as TDELink)._self, - }) - ); - return tooltipContent; - } - return null; + return tooltipContent; + } + if (resolvedAs === 'resource') { + const result = results as TDELink; + tooltipContent.appendChild( + createTooltipNode({ + tag: result.resource + ? `${result.resource?.[0]}/${result.resource?.[1]}` + : null, + title: result.title ?? result._self, + isDownloadable: result.isDownloadable, + }) + ); + return tooltipContent; + } + if (resolvedAs === 'resources') { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'Multiple', + title: `${ + (results as TDELink[]).length + } resources was found, click to list them`, + }) + ); + return tooltipContent; + } + if (resolvedAs === 'external') { + tooltipContent.appendChild( + createTooltipNode({ + tag: 'External', + title: (results as TDELink).title ?? (results as TDELink)._self, + }) + ); + return tooltipContent; + } + return null; } function createPopoverContent({ - results, - onClick, + results, + onClick, }: { - results: TDELink[]; - onClick: (result: TDELink) => void; + results: TDELink[]; + onClick: (result: TDELink) => void; }) { - const tooltipContent = document.createElement('div'); - tooltipContent.className = clsx( - `${CODEMIRROR_HOVER_CLASS}-resources-content` - ); - // create node for each link in results and then append it to the tooltipContent - (results as TDELink[]).forEach((link: TDELink) => { - const linkNode = createTooltipNode({ - tag: link.resource ? `${link.resource?.[0]}/${link.resource?.[1]}` : null, - title: link.title ?? link._self, - isDownloadable: link.isDownloadable, - }); - linkNode.onclick = () => { - removePopoversFromDOM(); - onClick(link); - }; - return tooltipContent.appendChild(linkNode); + const tooltipContent = document.createElement('div'); + tooltipContent.className = clsx( + `${CODEMIRROR_HOVER_CLASS}-resources-content` + ); + // create node for each link in results and then append it to the tooltipContent + (results as TDELink[]).forEach((link: TDELink) => { + const linkNode = createTooltipNode({ + tag: link.resource ? `${link.resource?.[0]}/${link.resource?.[1]}` : null, + title: link.title ?? link._self, + isDownloadable: link.isDownloadable, }); - return tooltipContent; + linkNode.onclick = () => { + removePopoversFromDOM(); + onClick(link); + }; + return tooltipContent.appendChild(linkNode); + }); + return tooltipContent; } function useEditorTooltip({ - ref, - isEditing, - orgLabel, - projectLabel, + ref, + isEditing, + orgLabel, + projectLabel, }: { - ref: React.MutableRefObject; - isEditing: boolean; - orgLabel: string; - projectLabel: string; + ref: React.MutableRefObject; + isEditing: boolean; + orgLabel: string; + projectLabel: string; }) { - const nexus = useNexusContext(); - const { - config: { apiEndpoint }, - } = useSelector((state: RootState) => ({ - config: state.config, - })); - - const allowTooltip = !isEditing; + const nexus = useNexusContext(); + const { + config: { apiEndpoint }, + } = useSelector((state: RootState) => ({ + config: state.config, + })); - React.useEffect(() => { - const currentEditor = (ref as React.MutableRefObject) - ?.current; - const editorWrapper = currentEditor.getWrapperElement(); - function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { - const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); - const tooltipRect = tooltip.getBoundingClientRect(); - if (tooltipRect.height <= editorRect.top) { - tooltip.style.top = `${editorRect.top - tooltipRect.height}px`; - } else { - tooltip.style.top = `${editorRect.bottom}px`; - } - tooltip.style.left = `${editorRect.left}px`; - } - function removeTooltipFromDom(tooltip: HTMLDivElement) { - if (tooltip.parentNode) { - tooltip.parentNode.removeChild(tooltip); - } - } - function hideTooltip(tooltip: HTMLDivElement) { - if (!tooltip.parentNode) { - return; - } - setTimeout(() => { - removeTooltipFromDom(tooltip); - }, 300); - } - function showTooltip(content: HTMLDivElement, node: HTMLElement) { - const tooltip = document.createElement('div'); - tooltip.className = CODEMIRROR_HOVER_CLASS; - tooltip.appendChild(content); - document.body.appendChild(tooltip); + const allowTooltip = !isEditing; - function hide() { - if (tooltip) { - node.classList.remove('has-tooltip'); - hideTooltip(tooltip); - tooltip.remove(); - } - node.removeEventListener('mouseout', hide); - node.removeEventListener('click', hide); - node.removeEventListener('scroll', hide); - } + React.useEffect(() => { + const currentEditor = (ref as React.MutableRefObject) + ?.current; + const editorWrapper = currentEditor.getWrapperElement(); + function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { + const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + if (tooltipRect.height <= editorRect.top) { + tooltip.style.top = `${editorRect.top - tooltipRect.height}px`; + } else { + tooltip.style.top = `${editorRect.bottom}px`; + } + tooltip.style.left = `${editorRect.left}px`; + } + function removeTooltipFromDom(tooltip: HTMLDivElement) { + if (tooltip.parentNode) { + tooltip.parentNode.removeChild(tooltip); + } + } + function hideTooltip(tooltip: HTMLDivElement) { + if (!tooltip.parentNode) { + return; + } + setTimeout(() => { + removeTooltipFromDom(tooltip); + }, 300); + } + function showTooltip(content: HTMLDivElement, node: HTMLElement) { + const tooltip = document.createElement('div'); + tooltip.className = CODEMIRROR_HOVER_CLASS; + tooltip.appendChild(content); + document.body.appendChild(tooltip); - node.addEventListener('mouseout', hide); - node.addEventListener('click', hide); - node.addEventListener('scroll', hide); + function hide() { + if (tooltip) { + node.classList.remove('has-tooltip'); + hideTooltip(tooltip); + tooltip.remove(); + } + node.removeEventListener('mouseout', hide); + node.removeEventListener('click', hide); + node.removeEventListener('scroll', hide); + } - const pool: ReturnType = setTimeout(() => { - if (tooltip) { - hideTooltip(tooltip); - tooltip.remove(); - } - return clearTimeout(pool); - }, 3000); + node.addEventListener('mouseout', hide); + node.addEventListener('click', hide); + node.addEventListener('scroll', hide); - return tooltip; + const pool: ReturnType = setTimeout(() => { + if (tooltip) { + hideTooltip(tooltip); + tooltip.remove(); } + return clearTimeout(pool); + }, 3000); - async function onMouseOver(ev: MouseEvent) { - const node = ev.target as HTMLElement; - if (node) { - const { token } = getTokenAndPosAt(ev, currentEditor); - const content = token?.string || ''; - const url = content.replace(/\\/g, '').replace(/\"/g, ''); - if (url && mayBeResolvableLink(url)) { - node.classList.add('wait-for-tooltip'); - removeTooltipsFromDOM(); - editorLinkResolutionHandler({ - nexus, - apiEndpoint, - url, - orgLabel, - projectLabel, - }).then(({ resolvedAs, results, error }) => { - const tooltipContent = createTooltipContent({ - resolvedAs, - error, - results, - }); - if (tooltipContent) { - node.classList.remove('wait-for-tooltip'); - node.classList.add( - resolvedAs === 'error' - ? 'error' - : resolvedAs === 'resource' && - (results as TDELink).isDownloadable - ? 'downloadable' - : 'has-tooltip' - ); - const tooltip = showTooltip(tooltipContent, node); - const calculatePosition = (ev: MouseEvent) => - positionner(ev, tooltip); - editorWrapper.addEventListener('mousemove', calculatePosition); - } - }); - } + return tooltip; + } + + async function onMouseOver(ev: MouseEvent) { + const node = ev.target as HTMLElement; + if (node) { + const { token } = getTokenAndPosAt(ev, currentEditor); + const content = token?.string || ''; + const url = content.replace(/\\/g, '').replace(/\"/g, ''); + if (url && mayBeResolvableLink(url)) { + node.classList.add('wait-for-tooltip'); + removeTooltipsFromDOM(); + editorLinkResolutionHandler({ + nexus, + apiEndpoint, + url, + orgLabel, + projectLabel, + }).then(({ resolvedAs, results, error }) => { + const tooltipContent = createTooltipContent({ + resolvedAs, + error, + results, + }); + if (tooltipContent) { + node.classList.remove('wait-for-tooltip'); + node.classList.add( + resolvedAs === 'error' + ? 'error' + : resolvedAs === 'resource' && + (results as TDELink).isDownloadable + ? 'downloadable' + : 'has-tooltip' + ); + const tooltip = showTooltip(tooltipContent, node); + const calculatePosition = (ev: MouseEvent) => + positionner(ev, tooltip); + editorWrapper.addEventListener('mousemove', calculatePosition); } + }); } - // allow the tooltip only when the editor is not in edition mode - // and the popover is not open - allowTooltip && editorWrapper.addEventListener('mouseover', onMouseOver); - // remove the event listener when not allwoed - !allowTooltip && - editorWrapper.removeEventListener('mouseover', onMouseOver); + } + } + // allow the tooltip only when the editor is not in edition mode + // and the popover is not open + allowTooltip && editorWrapper.addEventListener('mouseover', onMouseOver); + // remove the event listener when not allwoed + !allowTooltip && + editorWrapper.removeEventListener('mouseover', onMouseOver); - // cleanup - // remove the event listener when the component is unmounted - return () => { - allowTooltip && - editorWrapper.removeEventListener('mouseover', onMouseOver); - }; - }, [ - (ref as React.MutableRefObject)?.current, - allowTooltip, - ]); + // cleanup + // remove the event listener when the component is unmounted + return () => { + allowTooltip && + editorWrapper.removeEventListener('mouseover', onMouseOver); + }; + }, [ + (ref as React.MutableRefObject)?.current, + allowTooltip, + ]); } function useEditorPopover({ - ref, - orgLabel, - projectLabel, + ref, + orgLabel, + projectLabel, }: { - ref: React.MutableRefObject; - orgLabel: string; - projectLabel: string; + ref: React.MutableRefObject; + orgLabel: string; + projectLabel: string; }) { - const nexus = useNexusContext(); - const { - navigateResourceHandler, - downloadBinaryAsyncHandler, - } = useResolutionActions(); - const { - config: { apiEndpoint }, - } = useSelector((state: RootState) => ({ - config: state.config, - })); + const nexus = useNexusContext(); + const { + navigateResourceHandler, + downloadBinaryAsyncHandler, + } = useResolutionActions(); + const { + config: { apiEndpoint }, + } = useSelector((state: RootState) => ({ + config: state.config, + })); - React.useEffect(() => { - const currentEditor = (ref as React.MutableRefObject) - ?.current; - const editorWrapper = currentEditor.getWrapperElement(); - function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { - const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); - const tooltipRect = tooltip.getBoundingClientRect(); - if (tooltipRect.height <= editorRect.top) { - tooltip.style.top = `${editorRect.top - tooltipRect.height}px`; - } else { - tooltip.style.top = `${editorRect.bottom}px`; - } - tooltip.style.left = `${editorRect.left}px`; - } - function showTooltip(content: HTMLDivElement, node: HTMLElement) { - const tooltip = document.createElement('div'); - tooltip.className = `${CODEMIRROR_HOVER_CLASS}-popover popover`; - tooltip.appendChild(content); - document.body.appendChild(tooltip); - return tooltip; - } - function onEditorMouseDown(ev: MouseEvent, node: HTMLElement) { - if ( - ev.target && - !node.contains(ev.target as HTMLElement) && - !(ev.target as HTMLElement).isEqualNode(node) && - (ev.target as HTMLElement).closest('.CodeMirror-wrap') - ) { - removePopoversFromDOM(); - } - editorWrapper.removeEventListener('mousedown', (ev: MouseEvent) => - onEditorMouseDown(ev, node) - ); - } - async function onMouseDown(_: CodeMirror.Editor, ev: MouseEvent) { - const node = ev.target as HTMLElement; - if (node) { - const { token } = getTokenAndPosAt(ev, currentEditor); - const content = token?.string || ''; - const url = content.replace(/\\/g, '').replace(/\"/g, ''); - if (url && mayBeResolvableLink(url)) { - editorLinkResolutionHandler({ - nexus, - apiEndpoint, - url, - orgLabel, - projectLabel, - }).then(({ resolvedAs, results }) => { - switch (resolvedAs) { - case 'resources': { - const tooltipContent = createPopoverContent({ - results: results as TDELink[], - onClick: navigateResourceHandler, - }); - if (tooltipContent) { - const tooltip = showTooltip(tooltipContent, node); - positionner(ev, tooltip); - editorWrapper.addEventListener( - 'mousedown', - (ev: MouseEvent) => onEditorMouseDown(ev, node) - ); - } - break; - } - case 'resource': { - 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 navigateResourceHandler(result); - } - case 'external': { - window.open( - (results as TDELink)._self, - '_blank', - 'noopener noreferrer' - ); - break; - } - case 'error': - default: - break; - } - return; - }); + React.useEffect(() => { + const currentEditor = (ref as React.MutableRefObject) + ?.current; + const editorWrapper = currentEditor.getWrapperElement(); + function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { + const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + if (tooltipRect.height <= editorRect.top) { + tooltip.style.top = `${editorRect.top - tooltipRect.height}px`; + } else { + tooltip.style.top = `${editorRect.bottom}px`; + } + tooltip.style.left = `${editorRect.left}px`; + } + function showTooltip(content: HTMLDivElement, node: HTMLElement) { + const tooltip = document.createElement('div'); + tooltip.className = `${CODEMIRROR_HOVER_CLASS}-popover popover`; + tooltip.appendChild(content); + document.body.appendChild(tooltip); + return tooltip; + } + function onEditorMouseDown(ev: MouseEvent, node: HTMLElement) { + if ( + ev.target && + !node.contains(ev.target as HTMLElement) && + !(ev.target as HTMLElement).isEqualNode(node) && + (ev.target as HTMLElement).closest('.CodeMirror-wrap') + ) { + removePopoversFromDOM(); + } + editorWrapper.removeEventListener('mousedown', (ev: MouseEvent) => + onEditorMouseDown(ev, node) + ); + } + async function onMouseDown(_: CodeMirror.Editor, ev: MouseEvent) { + const node = ev.target as HTMLElement; + if (node) { + const { token } = getTokenAndPosAt(ev, currentEditor); + const content = token?.string || ''; + const url = content.replace(/\\/g, '').replace(/\"/g, ''); + if (url && mayBeResolvableLink(url)) { + editorLinkResolutionHandler({ + nexus, + apiEndpoint, + url, + orgLabel, + projectLabel, + }).then(({ resolvedAs, results }) => { + switch (resolvedAs) { + case 'resources': { + const tooltipContent = createPopoverContent({ + results: results as TDELink[], + onClick: navigateResourceHandler, + }); + if (tooltipContent) { + const tooltip = showTooltip(tooltipContent, node); + positionner(ev, tooltip); + editorWrapper.addEventListener( + 'mousedown', + (ev: MouseEvent) => onEditorMouseDown(ev, node) + ); } + break; + } + case 'resource': { + 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 navigateResourceHandler(result); + } + case 'external': { + window.open( + (results as TDELink)._self, + '_blank', + 'noopener noreferrer' + ); + break; + } + case 'error': + default: + break; } + return; + }); } - currentEditor.on('mousedown', onMouseDown); - return () => { - currentEditor.off('mousedown', onMouseDown); - }; - }, [ - (ref as React.MutableRefObject)?.current, - navigateResourceHandler, - ]); + } + } + currentEditor.on('mousedown', onMouseDown); + return () => { + currentEditor.off('mousedown', onMouseDown); + }; + }, [ + (ref as React.MutableRefObject)?.current, + navigateResourceHandler, + ]); } export { useEditorPopover, useEditorTooltip }; From 047900bad8a483cfd9e182c842d0f603a194b3cc Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Fri, 7 Jul 2023 10:10:18 +0200 Subject: [PATCH 106/192] Hotfix // Remove whitespace over header pictures in pages --- src/shared/layouts/FusionMainLayout.less | 4 ---- src/subapps/dataExplorer/styles.less | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/shared/layouts/FusionMainLayout.less b/src/shared/layouts/FusionMainLayout.less index 736196f42..b80760413 100644 --- a/src/shared/layouts/FusionMainLayout.less +++ b/src/shared/layouts/FusionMainLayout.less @@ -15,10 +15,6 @@ left: 80px; } - .ant-layout-content { - margin-top: 52px; - } - .ant-menu-item, .ant-menu-inline .ant-menu-item, .ant-menu-inline .ant-menu-item:not(:last-child) { diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 12580ccd3..1c5f92844 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -5,7 +5,7 @@ padding-left: 40px; background: @fusion-main-bg; width: 100%; - margin-top: 60px; + margin-top: 100px; .data-explorer-filters { display: flex; From 1c83eb631c0a6c42236c7368c67fc643af318b2f Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Wed, 5 Jul 2023 17:55:57 +0200 Subject: [PATCH 107/192] US 4022 / Show count in a clearer format Signed-off-by: Dinika Saxena --- package.json | 7 ++- .../dataExplorer/DataExplorer.spec.tsx | 60 +++++++++++-------- src/subapps/dataExplorer/DataExplorer.tsx | 28 +++------ src/subapps/dataExplorer/DatasetCount.tsx | 38 ++++++++++++ src/subapps/dataExplorer/styles.less | 2 +- 5 files changed, 87 insertions(+), 48 deletions(-) create mode 100644 src/subapps/dataExplorer/DatasetCount.tsx diff --git a/package.json b/package.json index 80c4ce8d3..492a5ab40 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", @@ -210,7 +210,10 @@ ], "globals": { "FUSION_VERSION": "1.0.0", - "COMMIT_HASH": "9013fa343" + "COMMIT_HASH": "9013fa343", + "ts-jest": { + "isolatedModules": true + } }, "watchPathIgnorePatterns": [ "node_modules" diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index a412c2810..021e2c42c 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -32,7 +32,7 @@ import { Provider } from 'react-redux'; import configureStore from '../../shared/store'; describe('DataExplorer', () => { - const defaultTotalResults = 500; + const defaultTotalResults = 500_123; const server = setupServer( dataExplorerPageHandler(defaultMockResult, defaultTotalResults), @@ -225,27 +225,35 @@ describe('DataExplorer', () => { ); }; - const getTotalCountFromBackend = async ( - expectedCount = defaultTotalResults - ) => { - const totalFromBackend = await screen.getByText('Total from backend:'); + const getTotalSizeOfDataset = async (expectedCount: string) => { + const totalFromBackend = await screen.getByText('Total:'); const totalCount = within(totalFromBackend).getByText( new RegExp(`${expectedCount} dataset`, 'i') ); return totalCount; }; - const getTotalFromFrontend = async (expectedCount: number = 0) => { - const totalFromFrontend = await screen.queryByText('Total from frontend:'); - if (!totalFromFrontend) { - return totalFromFrontend; - } - const totalCount = within(totalFromFrontend).getByText( - new RegExp(`${expectedCount} dataset`, 'i') + const getSizeOfCurrentlyLoadedData = async (expectedCount: number) => { + const totalFromBackend = await screen.getByText( + 'Sample loaded for review:' + ); + const totalCount = within(totalFromBackend).getByText( + new RegExp(`${expectedCount}`, 'i') ); return totalCount; }; + const getFilteredResultsCount = async (expectedCount: number = 0) => { + const filteredCountLabel = await screen.queryByText('Filtered:'); + if (!filteredCountLabel) { + return filteredCountLabel; + } + const filteredCount = within(filteredCountLabel).getByText( + new RegExp(`${expectedCount}`, 'i') + ); + return filteredCount; + }; + const updateResourcesShownInTable = async (resources: Resource[]) => { await expectRowCountToBe(10); await getRowsForNextPage(resources); @@ -542,37 +550,39 @@ describe('DataExplorer', () => { expect(history.location.pathname).toContain('self1'); }); - it('shows total results received from backend', async () => { + it('shows total size of dataset', async () => { await expectRowCountToBe(10); - const totalBackendCount = await getTotalCountFromBackend( - defaultTotalResults - ); + const totalBackendCount = await getTotalSizeOfDataset('500,123'); expect(totalBackendCount).toBeVisible(); }); + it('shows results currently loaded in frontend', async () => { + await expectRowCountToBe(10); + const loadedDataCount = await getSizeOfCurrentlyLoadedData(10); + expect(loadedDataCount).toBeVisible(); + }); + it('shows updated total from backend when user searches by project', async () => { await expectRowCountToBe(10); - const totalBackendBefore = await getTotalCountFromBackend( - defaultTotalResults - ); + const totalBackendBefore = await getTotalSizeOfDataset('500,123'); expect(totalBackendBefore).toBeVisible(); await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); await expectRowCountToBe(2); - const totalBackendAfter = await getTotalCountFromBackend(2); + const totalBackendAfter = await getTotalSizeOfDataset('2'); expect(totalBackendAfter).toBeVisible(); }); - it('does not show total frontend count if predicate is not selected', async () => { + it('does not show filtered count if predicate is not selected', async () => { await expectRowCountToBe(10); - const totalFromFrontend = await getTotalFromFrontend(); + const totalFromFrontend = await getFilteredResultsCount(); expect(totalFromFrontend).toEqual(null); }); - it('does shows total frontend count if predicate is selected', async () => { + it('shows total filtered count if predicate is selected', async () => { await expectRowCountToBe(10); - const totalFromFrontendBefore = await getTotalFromFrontend(); + const totalFromFrontendBefore = await getFilteredResultsCount(); expect(totalFromFrontendBefore).toEqual(null); await updateResourcesShownInTable([ @@ -586,7 +596,7 @@ describe('DataExplorer', () => { await selectOptionFromMenu(PredicateMenuLabel, EXISTS); await expectRowCountToBe(2); - const totalFromFrontendAfter = await getTotalFromFrontend(2); + const totalFromFrontendAfter = await getFilteredResultsCount(2); expect(totalFromFrontendAfter).toBeVisible(); }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 95253fe18..95feab006 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -8,7 +8,7 @@ import { DataExplorerTable } from './DataExplorerTable'; import './styles.less'; import { ProjectSelector } from './ProjectSelector'; import { PredicateSelector } from './PredicateSelector'; -import * as pluralize from 'pluralize'; +import { DatasetCount } from './DatasetCount'; export interface DataExplorerConfiguration { pageSize: number; @@ -91,25 +91,13 @@ export const DataExplorer: React.FC<{}> = () => {
{!isLoading && ( -
- - Total from backend:{' '} - - {resources?._total ?? 0}{' '} - {pluralize('dataset', resources?._total ?? 0)} - {' '} - - - {predicateFilter !== null && ( - - Total from frontend:{' '} - - {displayedDataSource.length}{' '} - {pluralize('dataset', displayedDataSource.length ?? 0)} - {' '} - - )} -
+ )} = ({ + nexusTotal, + totalOnPage, + totalFiltered, +}: Props) => { + return ( +
+ + Total:{' '} + + {nexusTotal.toLocaleString(`en-US`)}{' '} + {pluralize('dataset', nexusTotal ?? 0)} + {' '} + + + + Sample loaded for review: {totalOnPage} + + + {!isNil(totalFiltered) && ( + + Filtered: {totalFiltered} + + )} +
+ ); +}; diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 1c5f92844..d39b5f1cf 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -13,7 +13,7 @@ } .data-explorer-count { - color: @fusion-blue-8; + color: @fusion-neutral-7; margin-left: 20px; margin-bottom: 28px; From d530c57d5c1b1c0b2945e2300bf8074ae06a7b46 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 15:39:13 +0200 Subject: [PATCH 108/192] de-14/ref: rename hide fn to cleanup de-14/ref: remove the removeFromDom fn --- .../ResourceEditor/useEditorTooltip.tsx | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index d8d5f6fc4..5ce7a48a7 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -165,6 +165,7 @@ function useEditorTooltip({ const currentEditor = (ref as React.MutableRefObject) ?.current; const editorWrapper = currentEditor.getWrapperElement(); + function positionner(ev: MouseEvent, tooltip: HTMLDivElement) { const editorRect = (ev.target as HTMLElement).getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); @@ -175,17 +176,15 @@ function useEditorTooltip({ } tooltip.style.left = `${editorRect.left}px`; } - function removeTooltipFromDom(tooltip: HTMLDivElement) { - if (tooltip.parentNode) { - tooltip.parentNode.removeChild(tooltip); - } - } + function hideTooltip(tooltip: HTMLDivElement) { if (!tooltip.parentNode) { return; } setTimeout(() => { - removeTooltipFromDom(tooltip); + if (tooltip.parentNode) { + tooltip.parentNode.removeChild(tooltip); + } }, 300); } function showTooltip(content: HTMLDivElement, node: HTMLElement) { @@ -194,27 +193,27 @@ function useEditorTooltip({ tooltip.appendChild(content); document.body.appendChild(tooltip); - function hide() { + function cleanup() { if (tooltip) { node.classList.remove('has-tooltip'); hideTooltip(tooltip); tooltip.remove(); } - node.removeEventListener('mouseout', hide); - node.removeEventListener('click', hide); - node.removeEventListener('scroll', hide); + node.removeEventListener('mouseout', cleanup); + node.removeEventListener('click', cleanup); + node.removeEventListener('scroll', cleanup); } - node.addEventListener('mouseout', hide); - node.addEventListener('click', hide); - node.addEventListener('scroll', hide); + node.addEventListener('mouseout', cleanup); + node.addEventListener('click', cleanup); + node.addEventListener('scroll', cleanup); - const pool: ReturnType = setTimeout(() => { + const timeoutId: ReturnType = setTimeout(() => { if (tooltip) { hideTooltip(tooltip); tooltip.remove(); } - return clearTimeout(pool); + return clearTimeout(timeoutId); }, 3000); return tooltip; From a51d8676bc2aa77703a4d36b7631547d75e1757b Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 15:50:21 +0200 Subject: [PATCH 109/192] de-14/fix: add allowed protocals for links detection --- src/shared/components/ResourceEditor/index.tsx | 8 +++++++- src/utils/validUrl.ts | 12 +++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 1849217c8..ef38c4660 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -16,6 +16,7 @@ import 'codemirror/addon/fold/foldgutter'; import 'codemirror/addon/fold/brace-fold'; import isValidUrl, { + isAllowedProtocal, isStorageLink, isUrlCurieFormat, } from '../../../utils/validUrl'; @@ -53,7 +54,12 @@ export interface ResourceEditorProps { const switchMarginRight = { marginRight: 5 }; const isClickableLine = (url: string) => { - return isValidUrl(url) && !isUrlCurieFormat(url) && !isStorageLink(url); + return ( + isValidUrl(url) && + isAllowedProtocal(url) && + !isUrlCurieFormat(url) && + !isStorageLink(url) + ); }; const ResourceEditor: React.FunctionComponent = props => { diff --git a/src/utils/validUrl.ts b/src/utils/validUrl.ts index 339b1480d..cc641c87e 100644 --- a/src/utils/validUrl.ts +++ b/src/utils/validUrl.ts @@ -34,5 +34,15 @@ function isExternalLink(url: string): boolean { function isStorageLink(url: string): boolean { return url.startsWith('file:///gpfs'); } -export { easyValidURL, isUrlCurieFormat, isExternalLink, isStorageLink }; +function isAllowedProtocal(url: string): boolean { + return url.startsWith('https://') || url.startsWith('http://'); +} + +export { + easyValidURL, + isUrlCurieFormat, + isExternalLink, + isStorageLink, + isAllowedProtocal, +}; export default isValidUrl; From 82f35f339ef587e017fbbaf755f7bcd4ef60daa1 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 16:05:43 +0200 Subject: [PATCH 110/192] de-14/clean: remove unnecessary code --- jest.setup.js | 1 - 1 file changed, 1 deletion(-) diff --git a/jest.setup.js b/jest.setup.js index 70b0d0273..c77cd3cef 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,4 +1,3 @@ -const { rest } = require('msw'); fetch = require('node-fetch'); window.fetch = fetch; From 929b2c7bc0656f2b7179c9e555c8d37d1ac15aed Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:08:57 +0200 Subject: [PATCH 111/192] d-4044/update: add new data explorer gf state structure and types --- src/server/index.tsx | 5 +- src/shared/store/reducers/data-explorer.ts | 267 +++++++++++++++------ 2 files changed, 198 insertions(+), 74 deletions(-) diff --git a/src/server/index.tsx b/src/server/index.tsx index a18dbcf7d..379d42283 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -148,10 +148,9 @@ app.get('*', async (req: express.Request, res: express.Response) => { modals: DEFAULT_MODALS_STATE, dataExplorer: { current: null, - links: [], - shrinked: false, + leftNodes: { links: [], shrinked: false }, + rightNodes: { links: [], shrinked: false }, limited: false, - highlightIndex: -1, }, }; diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index f4553ec65..8856d169f 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -1,5 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; -import { slice, omit, clone, dropRight, nth } from 'lodash'; +import { slice, clone, dropRight, nth, last, concat } from 'lodash'; type TProject = string; type TOrganization = string; @@ -7,9 +7,20 @@ type TResourceID = string; type TVersionTag = number; type TMediaType = string; -export type TDEResource = - | [TOrganization, TProject, TResourceID, TVersionTag] - | [TOrganization, TProject, TResourceID, TVersionTag, TMediaType]; +export type TDEResourceWithoutMedia = [ + TOrganization, + TProject, + TResourceID, + TVersionTag +]; +export type TDEResourceWithMedia = [ + TOrganization, + TProject, + TResourceID, + TVersionTag, + TMediaType +]; +export type TDEResource = TDEResourceWithoutMedia | TDEResourceWithMedia; export type TDELink = { _self: string; @@ -18,46 +29,50 @@ export type TDELink = { resource?: TDEResource; isDownloadable?: boolean; }; + export type TDataExplorerState = { - links: TDELink[]; + leftNodes: { + links: TDELink[]; + shrinked: boolean; + }; + rightNodes: { + links: TDELink[]; + shrinked: boolean; + }; current: TDELink | null; referer?: { pathname: string; search: string; state: Record; } | null; - shrinked: boolean; limited: boolean; - highlightIndex: number; }; +export type TNavigationStackSide = 'left' | 'right'; +export const DATA_EXPLORER_GRAPH_FLOW_PATH = '/data-explorer/graph-flow'; +export const DATA_EXPLORER_GRAPH_FLOW_DIGEST = 'data-explorer-last-navigation'; +export const MAX_NAVIGATION_ITEMS_IN_STACK = 3; + const initialState: TDataExplorerState = { - links: [], + leftNodes: { links: [], shrinked: false }, + rightNodes: { links: [], shrinked: false }, current: null, referer: null, - shrinked: false, limited: false, - highlightIndex: -1, }; -export const DATA_EXPLORER_GRAPH_FLOW_PATH = '/data-explorer/graph-flow'; -export const DATA_EXPLORER_GRAPH_FLOW_DIGEST = 'data-explorer-last-navigation'; -export const MAX_NAVIGATION_ITEMS_IN_STACK = 5; const calculateNewDigest = (state: TDataExplorerState) => { const clonedState = clone(state); - const digest = btoa( - JSON.stringify({ - ...clonedState, - links: clonedState.links.map(i => omit(i, ['highlight'])), - current: omit(clonedState.current, ['highlight']), - }) - ); + const digest = btoa(JSON.stringify(clonedState)); sessionStorage.setItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST, digest); }; const isShrinkable = (links: TDELink[]) => { return links.length > MAX_NAVIGATION_ITEMS_IN_STACK; }; +function insert(arr: any[], index: number, item: any) { + arr.splice(index, 0, item); +} export const dataExplorerSlice = createSlice({ initialState, @@ -69,7 +84,14 @@ export const dataExplorerSlice = createSlice({ try { return { ...newState, - shrinked: isShrinkable(newState.links), + leftNodes: { + links: newState.leftNodes.links, + shrinked: isShrinkable(newState.leftNodes.links), + }, + rightNodes: { + links: newState.rightNodes.links, + shrinked: isShrinkable(newState.rightNodes.links), + }, }; } catch (error) { return state; @@ -84,88 +106,192 @@ export const dataExplorerSlice = createSlice({ referer, current, limited, - links: - source && current - ? source._self === current._self - ? [] - : [source] - : [], + leftNodes: { + links: + source && current + ? source._self === current._self + ? [] + : [source] + : [], + shrinked: false, + }, }; calculateNewDigest(newState); return newState; }, AddNewNodeDataExplorerGraphFlow: (state, action) => { - const linkIndex = state.links.findIndex( - item => item._self === action.payload._self - ); - const isCurrentLink = state.current?._self === action.payload._self; - const newLinks = isCurrentLink - ? state.links - : linkIndex !== -1 - ? state.links - : [...state.links, state.current!]; - const newCurrent = - isCurrentLink || linkIndex !== -1 ? state.current : action.payload; - const newState: TDataExplorerState = { + if (action.payload._self === state.current?._self) { + console.log('@@same node'); + return state; + } + const newLink = action.payload as TDELink; + const whichSide = state.leftNodes.links.find( + link => link._self === newLink._self + ) + ? 'left' + : state.rightNodes.links.find(link => link._self === newLink._self) + ? 'right' + : null; + let leftNodesLinks: TDELink[] = []; + let rightNodesLinks: TDELink[] = []; + let current: TDELink; + switch (whichSide) { + case 'left': { + const index = state.leftNodes.links.findIndex( + link => link._self === newLink._self + ); + rightNodesLinks = concat( + slice(state.leftNodes.links, index + 1), + state.current ? [state.current] : [], + state.rightNodes.links + ); + leftNodesLinks = slice(state.leftNodes.links, 0, index); + current = state.leftNodes.links[index]; + break; + } + case 'right': { + const index = state.rightNodes.links.findIndex( + link => link._self === newLink._self + ); + // make the new link the current one + // add the links before that and the current one the left part + leftNodesLinks = concat( + state.leftNodes.links, + state.current ? [state.current] : [], + slice(state.rightNodes.links, 0, index) + ); + rightNodesLinks = slice(state.rightNodes.links, index + 1); + current = state.rightNodes.links[index]; + break; + } + case null: + default: { + leftNodesLinks = concat( + state.leftNodes.links, + state.current ? [state.current] : [] + ); + rightNodesLinks = []; + current = action.payload; + break; + } + } + const newState = { ...state, - links: newLinks, - current: newCurrent, - highlightIndex: linkIndex, - shrinked: linkIndex !== -1 ? false : isShrinkable(newLinks), + current, + leftNodes: { + links: leftNodesLinks, + shrinked: isShrinkable(leftNodesLinks), + }, + rightNodes: { + links: rightNodesLinks, + shrinked: isShrinkable(rightNodesLinks), + }, }; calculateNewDigest(newState); return newState; }, JumpToNodeDataExplorerGraphFlow: (state, action) => { - const newLinks = slice(state.links, 0, action.payload); - const newState: TDataExplorerState = { + const index = action.payload.index as number; + const side = action.payload.side as TNavigationStackSide; + const realIndex = + side === 'left' ? index : state.leftNodes.links.length + index + 1; + const allLinks = concat( + state.leftNodes.links, + state.current ? [state.current] : [], + state.rightNodes.links + ); + const current = nth(allLinks, realIndex) as TDELink; + // construct left part + const leftNodesLinks = slice(allLinks, 0, realIndex); + const leftNodes = { + links: leftNodesLinks, + shrinked: isShrinkable(leftNodesLinks), + }; + // construct right part + const rightNodesLinks = slice(allLinks, realIndex + 1); + const rightNodes = { + links: rightNodesLinks, + shrinked: isShrinkable(rightNodesLinks), + }; + const newState = { ...state, - links: newLinks, - current: state.links[action.payload], - shrinked: isShrinkable(newLinks), + leftNodes, + rightNodes, + current, }; calculateNewDigest(newState); return newState; }, ReturnBackDataExplorerGraphFlow: state => { - const lastItem = state.links.length ? nth(state.links, -1) : null; - const newLinks = dropRight(state.links); - const newState = lastItem - ? { - ...state, - links: newLinks, - current: lastItem, - shrinked: isShrinkable(newLinks), - } - : state; + const current = last(state.leftNodes.links) as TDELink; + const newrightNodesLinks = state.rightNodes.links; + const newleftNodesLinks = dropRight(state.leftNodes.links) as TDELink[]; + insert(newrightNodesLinks, 0, state.current); + const rightNodes = { + links: newrightNodesLinks, + shrinked: isShrinkable(newrightNodesLinks), + }; + const newState = { + ...state, + current, + rightNodes, + leftNodes: { + links: newleftNodesLinks, + shrinked: isShrinkable(newleftNodesLinks), + }, + }; calculateNewDigest(newState); return newState; }, - ExpandNavigationStackDataExplorerGraphFlow: state => { + ExpandNavigationStackDataExplorerGraphFlow: (state, action) => { + const side = action.payload.side as TNavigationStackSide; + const sideUpdater = + side === 'left' + ? { + leftNodes: { + ...state.leftNodes, + shrinked: false, + }, + } + : { + rightNodes: { + ...state.rightNodes, + shrinked: false, + }, + }; const newState = { ...state, - shrinked: false, + ...sideUpdater, }; calculateNewDigest(newState); return newState; }, - ShrinkNavigationStackDataExplorerGraphFlow: state => { + ShrinkNavigationStackDataExplorerGraphFlow: (state, action) => { + const side = action.payload.side as TNavigationStackSide; + const sideUpdater = + side === 'left' + ? { + leftNodes: { + ...state.leftNodes, + shrinked: isShrinkable(state.leftNodes.links) ? true : false, + }, + } + : { + rightNodes: { + ...state.rightNodes, + shrinked: isShrinkable(state.rightNodes.links) ? true : false, + }, + }; const newState = { ...state, - shrinked: isShrinkable(state.links) ? true : false, + ...sideUpdater, }; calculateNewDigest(newState); return newState; }, - ResetDataExplorerGraphFlow: (state, action) => { + ResetDataExplorerGraphFlow: (_, action) => { return action.payload.initialState ?? initialState; }, - ResetHighlightedNodeDataExplorerGraphFlow: state => { - return { - ...state, - highlightIndex: -1, - }; - }, InitDataExplorerGraphFlowLimitedVersion: (state, action) => { const newState = { ...state, @@ -180,12 +306,11 @@ export const { PopulateDataExplorerGraphFlow, InitNewVisitDataExplorerGraphView, AddNewNodeDataExplorerGraphFlow, - JumpToNodeDataExplorerGraphFlow, ExpandNavigationStackDataExplorerGraphFlow, ShrinkNavigationStackDataExplorerGraphFlow, + JumpToNodeDataExplorerGraphFlow, ReturnBackDataExplorerGraphFlow, ResetDataExplorerGraphFlow, - ResetHighlightedNodeDataExplorerGraphFlow, InitDataExplorerGraphFlowLimitedVersion, } = dataExplorerSlice.actions; From de76c63388632ba41678c44d190ebd27b4cec487 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:09:41 +0200 Subject: [PATCH 112/192] d-4044/update: update navigation collapse btn for both sides --- src/shared/components/Icons/Collapse.tsx | 20 ++++++++++ .../NavigationCollapseButton.tsx | 39 +++++++++---------- 2 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 src/shared/components/Icons/Collapse.tsx diff --git a/src/shared/components/Icons/Collapse.tsx b/src/shared/components/Icons/Collapse.tsx new file mode 100644 index 000000000..34f92b90e --- /dev/null +++ b/src/shared/components/Icons/Collapse.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +const CollapseIcon = (props: React.SVGProps) => { + return ( + + + + ); +}; + +export default CollapseIcon; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx index 3fe021786..a638556dc 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx @@ -1,24 +1,23 @@ -import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { ShrinkOutlined } from '@ant-design/icons'; -import { RootState } from '../../store/reducers'; -import { - MAX_NAVIGATION_ITEMS_IN_STACK, - ShrinkNavigationStackDataExplorerGraphFlow, -} from '../../store/reducers/data-explorer'; -import './styles.less'; +import * as React from 'react'; +import { clsx } from 'clsx'; +import { Tooltip } from 'antd'; +import CollapseIcon from '../../components/Icons/Collapse'; +import { TNavigationStackSide } from '../../store/reducers/data-explorer'; -const NavigationHamburguer = () => { - const { shrinked, links } = useSelector( - (state: RootState) => state.dataExplorer +const NavigationCollapseButton = ({ + side, + onExpand, +}: { + side: TNavigationStackSide; + onExpand: () => void; +}) => { + return ( + + + ); - const dispatch = useDispatch(); - const onShrink = () => dispatch(ShrinkNavigationStackDataExplorerGraphFlow()); - return !shrinked && links.length > MAX_NAVIGATION_ITEMS_IN_STACK ? ( - - ) : null; }; -export default NavigationHamburguer; +export default NavigationCollapseButton; From 546908a78e1c31974e55c9220aee3670454b470c Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:10:23 +0200 Subject: [PATCH 113/192] d-4044/update: update navigation btns (simple/shrinked) and styles --- .../NavigationStackItem.tsx | 118 +++++++--- .../NavigationStackShrinkedItem.tsx | 53 ++--- .../styles.less | 204 +++++++++++------- .../NavigationStack.tsx | 71 +++--- .../styles.less | 7 +- .../useNavigationStack.ts | 40 ++++ 6 files changed, 327 insertions(+), 166 deletions(-) create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx index 7b35bf562..47e5e10a2 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -1,15 +1,19 @@ -import React from 'react'; +import * as React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router'; -import { Tag, Tooltip } from 'antd'; +import { Space, Tag, Tooltip } from 'antd'; import { clsx } from 'clsx'; import { isArray } from 'lodash'; -import { PlusOutlined } from '@ant-design/icons'; +import { FullscreenOutlined } from '@ant-design/icons'; import { TDEResource, JumpToNodeDataExplorerGraphFlow, + TNavigationStackSide, + MAX_NAVIGATION_ITEMS_IN_STACK, } from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; +import useNavigationStackManager from '../../organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack'; +import NavigationCollapseButton from './NavigationCollapseButton'; import './styles.less'; export type TNavigationStackItem = { @@ -18,7 +22,7 @@ export type TNavigationStackItem = { types?: string | string[]; title: string; resource?: TDEResource; - highlighted: boolean; + side: TNavigationStackSide; }; const NavigationStackItem = ({ @@ -27,46 +31,108 @@ const NavigationStackItem = ({ title, types, resource, - highlighted, + side, }: TNavigationStackItem) => { const dispatch = useDispatch(); const location = useLocation(); const history = useHistory(); - const { shrinked, links } = useSelector( + const { leftNodes, rightNodes } = useSelector( (state: RootState) => state.dataExplorer ); + const { + onRightShrink, + onLeftShrink, + leftShrinked, + rightShrinked, + } = useNavigationStackManager(); + const onClick = () => { - dispatch(JumpToNodeDataExplorerGraphFlow(index)); + dispatch(JumpToNodeDataExplorerGraphFlow({ index, side })); history.replace(location.pathname); }; + const parentNode = side === 'left' ? leftNodes : rightNodes; + const orgProject = + resource?.[0] && resource?.[1] && `${resource?.[0]}/${resource?.[1]}`; + const showLeftCollapseBtn = + side === 'left' && + !leftShrinked && + leftNodes.links.length > MAX_NAVIGATION_ITEMS_IN_STACK && + leftNodes.links.length - 1 === index; + const showRightCollapseBtn = + side === 'right' && + !rightShrinked && + rightNodes.links.length > MAX_NAVIGATION_ITEMS_IN_STACK && + index === 0; + + const collapseRightBtn = React.useCallback(() => { + return ( + showRightCollapseBtn && ( + + ) + ); + }, [showRightCollapseBtn, side, onRightShrink]); + + const collapseLeftBtn = React.useCallback(() => { + return ( + showLeftCollapseBtn && ( + + ) + ); + }, [showLeftCollapseBtn, side, onLeftShrink]); + return ( + {collapseLeftBtn()}
); }; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx index 80c26c26f..b31d44491 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx @@ -4,39 +4,42 @@ import { clsx } from 'clsx'; import { MoreOutlined, PlusOutlined } from '@ant-design/icons'; import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../store/reducers'; -import { ExpandNavigationStackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; import './styles.less'; +import { TDELink } from 'shared/store/reducers/data-explorer'; const BORDER_ITEMS = 2; -const NavigationStackShrinkedItem = () => { - const { links, shrinked, highlightIndex } = useSelector( - (state: RootState) => state.dataExplorer - ); - const dispatch = useDispatch(); +const NavigationStackShrinkedItem = ({ + side, + links, + shrinked, + onExpand, +}: { + side: 'left' | 'right'; + shrinked: boolean; + links: TDELink[]; + onExpand: () => void; +}) => { const count = links.length - BORDER_ITEMS; - const onClick = () => dispatch(ExpandNavigationStackDataExplorerGraphFlow()); return ( } - > - - -
{count}
- +
+ Open {count} other resources
} + > + + +
{count}
+ +
); }; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index 0342f81fa..a08136a77 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -1,26 +1,104 @@ @import '../../lib.less'; .navigation-stack-item { - display: grid; - grid-template-rows: repeat(4, max-content); - gap: 10px; - align-items: start; - justify-items: center; - justify-content: center; - align-content: start; - border-right: 1px solid @fusion-neutral-6; - padding: 3px 5px; - height: 100%; - min-height: 0; - background-color: @fusion-main-bg; + display: flex; + align-items: flex-start; - &.more { - background-color: white; + &.shrinkable { + display: none; } - &.no-more { - display: none; - transition: display 0.2s cubic-bezier(0.075, 0.82, 0.165, 1); + &__wrapper { + display: grid; + grid-template-rows: repeat(4, max-content); + gap: 10px; + align-items: start; + justify-items: center; + justify-content: center; + align-content: start; + padding: 3px 5px; + height: 100%; + min-height: 0; + background-color: @fusion-main-bg; + position: relative; + + &.more { + background-color: white; + } + + &.no-more { + display: none; + transition: display 0.2s cubic-bezier(0.075, 0.82, 0.165, 1); + } + + .icon { + margin-top: 5px !important; + padding: 4px !important; + } + + .title { + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 600; + font-size: 12px; + line-height: 130%; + letter-spacing: 0.01em; + color: #003a8c; + writing-mode: vertical-lr; + text-orientation: sideways-right; + transform: rotate(180deg); + align-self: start; + } + + .org-project { + writing-mode: vertical-rl; + transform: rotate(-180deg); + background-color: #bfbfbfbe; + padding: 5px 1px; + font-size: 10px; + border-radius: 4px; + user-select: none; + } + + .types { + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 300; + font-size: 12px; + line-height: 130%; + letter-spacing: 0.01em; + color: #8c8c8c; + writing-mode: vertical-lr; + text-orientation: mixed; + align-self: start; + transform: rotate(180deg); + } + + .count { + font-weight: 600; + font-size: 15px; + line-height: 130%; + letter-spacing: 0.01em; + color: #bfbfbf; + } + + .ellipsis { + user-select: none; + writing-mode: vertical-lr; + text-orientation: mixed; + } + } + + &.right { + .navigation-stack-item__wrapper { + border-left: 1px solid @fusion-neutral-6; + } + } + + &.left { + .navigation-stack-item__wrapper { + border-right: 1px solid @fusion-neutral-6; + } } &.highlight { @@ -33,65 +111,39 @@ animation: vibrate 0.3s linear infinite both; } - &.shrinkable { - display: none; - } + .collapse-btn { + inset: 0; + overflow: visible; + appearance: none; + -webkit-appearance: none; + padding: 8px 8px; + background-color: @fusion-blue-8; + display: flex; + align-items: center; + justify-content: center; + margin-top: 40px; + box-shadow: 0 2px 12px rgba(#333, 0.12); + cursor: pointer; + border: none; - .icon { - margin-top: 5px !important; - padding: 4px !important; - } - - .title { - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 600; - font-size: 12px; - line-height: 130%; - letter-spacing: 0.01em; - color: #003a8c; - writing-mode: vertical-lr; - text-orientation: sideways-right; - transform: rotate(180deg); - align-self: start; - } - - .org-project { - writing-mode: vertical-rl; - transform: rotate(-180deg); - background-color: #bfbfbfbe; - padding: 5px 1px; - font-size: 10px; - border-radius: 4px; - user-select: none; - } - - .types { - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 300; - font-size: 12px; - line-height: 130%; - letter-spacing: 0.01em; - color: #8c8c8c; - writing-mode: vertical-lr; - text-orientation: mixed; - align-self: start; - transform: rotate(180deg); - } + &.right { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + svg { + transform: rotate(180deg); + } + } - .count { - font-weight: 600; - font-size: 15px; - line-height: 130%; - letter-spacing: 0.01em; - color: #bfbfbf; - } + &.left { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } - .ellipsis { - user-select: none; - writing-mode: vertical-lr; - text-orientation: mixed; + svg { + color: white; + width: 16px; + height: 16px; + } } } @@ -130,6 +182,7 @@ color: @fusion-daybreak-7; } } + &.go-back-to-referer { margin-left: 40px; } @@ -192,22 +245,27 @@ -webkit-transform: translate(0); transform: translate(0); } + 20% { -webkit-transform: translate(-1.5px, 1.5px); transform: translate(-1.5px, 1.5px); } + 40% { -webkit-transform: translate(-1.5px, -1.5px); transform: translate(-1.5px, -1.5px); } + 60% { -webkit-transform: translate(1.5px, 1.5px); transform: translate(1.5px, 1.5px); } + 80% { -webkit-transform: translate(1.5px, -1.5px); transform: translate(1.5px, -1.5px); } + 100% { -webkit-transform: translate(0); transform: translate(0); diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx index 70ed128e7..95b679a46 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx @@ -1,78 +1,67 @@ -import React, { Fragment, useEffect, useRef } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import React, { Fragment } from 'react'; import { clsx } from 'clsx'; -import { RootState } from '../../store/reducers'; -import { ResetHighlightedNodeDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +import { TNavigationStackSide } from '../../store/reducers/data-explorer'; import { NavigationStackItem, NavigationStackShrinkedItem, } from '../../molecules/DataExplorerGraphFlowMolecules'; +import useNavigationStackManager from './useNavigationStack'; import './styles.less'; -import { animate, spring } from 'motion'; -const NavigationStack = () => { - const dispatch = useDispatch(); - const { links, shrinked, highlightIndex } = useSelector( - (state: RootState) => state.dataExplorer - ); - const shrinkedRef = useRef(shrinked); - const stackRef = useRef(null); - - if (highlightIndex !== -1) { - setTimeout(() => { - dispatch(ResetHighlightedNodeDataExplorerGraphFlow()); - }, 2000); - } - useEffect(() => { - if (shrinkedRef.current && !shrinked && stackRef.current) { - animate( - stackRef.current, - { scale: 1 }, - { easing: spring({ stiffness: 10, damping: 10, mass: 500 }) } - ); - } else if (stackRef.current) { - animate( - stackRef.current, - { display: 'grid' }, - { easing: spring({ stiffness: 300, damping: 10 }) } - ); - } - }, [shrinked, shrinkedRef.current]); +const NavigationStack = ({ side }: { side: TNavigationStackSide }) => { + const { + leftLinks, + rightLinks, + leftShrinked, + rightShrinked, + onRightExpand, + onLeftExpand, + } = useNavigationStackManager(); + const links = side === 'left' ? leftLinks : rightLinks; + const shrinked = side === 'left' ? leftShrinked : rightShrinked; + const onExpand = side === 'left' ? onLeftExpand : onRightExpand; return ( -
-
+
+
{links.map(({ title, types, _self, resource }, index) => { if (index === 0) { return ( - + {shrinked && ( + + )} ); } return ( ); diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less index 98ac85b59..5575a9b71 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less @@ -3,9 +3,14 @@ align-items: flex-start; position: fixed; gap: 10px; - left: 0; top: 52px; z-index: 6; + &.left { + left: 0; + } + &.right { + right: 0; + } } .navigation-stack { diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts new file mode 100644 index 000000000..d37d1e06b --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts @@ -0,0 +1,40 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory, useLocation } from 'react-router'; +import { RootState } from '../../store/reducers'; +import { + ExpandNavigationStackDataExplorerGraphFlow, + ShrinkNavigationStackDataExplorerGraphFlow, +} from '../../store/reducers/data-explorer'; + +const useNavigationStackManager = () => { + const dispatch = useDispatch(); + const { rightNodes, leftNodes } = useSelector( + (state: RootState) => state.dataExplorer + ); + const leftShrinked = leftNodes.shrinked; + const rightShrinked = rightNodes.shrinked; + const leftLinks = leftNodes.links; + const rightLinks = rightNodes.links; + + const onLeftShrink = () => + dispatch(ShrinkNavigationStackDataExplorerGraphFlow({ side: 'left' })); + const onLeftExpand = () => + dispatch(ExpandNavigationStackDataExplorerGraphFlow({ side: 'left' })); + const onRightShrink = () => + dispatch(ShrinkNavigationStackDataExplorerGraphFlow({ side: 'right' })); + const onRightExpand = () => + dispatch(ExpandNavigationStackDataExplorerGraphFlow({ side: 'right' })); + + return { + leftShrinked, + rightShrinked, + leftLinks, + rightLinks, + onLeftShrink, + onLeftExpand, + onRightShrink, + onRightExpand, + }; +}; + +export default useNavigationStackManager; From bc041287eb481d2482a2ab1ca7769498787df3db Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:11:11 +0200 Subject: [PATCH 114/192] d-4044/update: the de-gf page structure --- .../DateExplorerGraphFlow.tsx | 47 +++++++++++++------ .../canvas/DataExplorerGraphFlow/styles.less | 31 ++++++------ 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index 02a8fa33c..5413a260b 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -9,13 +9,10 @@ import { PopulateDataExplorerGraphFlow, ResetDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; -import { - NavigationBackButton, - NavigationCollapseButton, -} from '../../molecules/DataExplorerGraphFlowMolecules'; import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; import ResourceResolutionCache from '../../components/ResourceEditor/ResourcesLRUCache'; +import useNavigationStackManager from '../../organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack'; import './styles.less'; @@ -24,10 +21,21 @@ const DataExplorerGraphFlow = () => { const location = useLocation(); const dispatch = useDispatch(); const digestFirstRender = useRef(false); - const { links, shrinked, current } = useSelector( + const { current, rightNodes, leftNodes } = useSelector( (state: RootState) => state.dataExplorer ); + const { + leftShrinked, + rightShrinked, + leftLinks, + rightLinks, + onLeftShrink, + onLeftExpand, + onRightShrink, + onRightExpand, + } = useNavigationStackManager(); + useEffect(() => { if (!digestFirstRender.current) { const state = sessionStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); @@ -75,25 +83,34 @@ const DataExplorerGraphFlow = () => {
-
- -
-
- - -
+ {!!leftLinks.length && ( +
+ +
+ )}
+ {!!rightLinks.length && ( +
+ +
+ )}
); }; diff --git a/src/shared/canvas/DataExplorerGraphFlow/styles.less b/src/shared/canvas/DataExplorerGraphFlow/styles.less index f2a5b0a41..e0942bc71 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/styles.less +++ b/src/shared/canvas/DataExplorerGraphFlow/styles.less @@ -3,33 +3,34 @@ .data-explorer-resolver { width: 100%; max-width: 100%; + display: grid; + grid-auto-columns: 1fr; align-items: flex-start; justify-content: flex-start; background-color: @fusion-main-bg; - display: grid; - grid-auto-columns: 1fr; gap: 10px; &.no-links { grid-template-columns: 1fr; } - &.with-links { - grid-template-columns: calc(calc(var(--links-count) * 31px) + 40px) max-content 1fr; + &.left-existed { + grid-template-columns: calc(calc(var(--left--links-count) * 31px) + 40px) 1fr; + } + &.right-existed { + grid-template-columns: 1fr calc( + calc(var(--right--links-count) * 31px) + 40px + ); + } + &.left-existed.right-existed { + grid-template-columns: calc(calc(var(--left--links-count) * 31px) + 40px) 1fr calc( + calc(var(--right--links-count) * 31px) + 40px + ); + } } - .degf__navigation-stack { width: 100%; } - - .degf__navigation-back { - position: sticky; - top: 53px; - display: flex; - align-items: center; - gap: 10px; - } - .degf__content { padding: 30px 20px 10px; display: flex; @@ -37,8 +38,8 @@ align-items: flex-start; justify-content: space-between; gap: 10px; + transition: all 0.4s ease-in-out; } - &.no-current { height: 100%; width: 100%; From 24dd947e9cd6ddb955670ce7cec088cb0f5efa31 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:13:43 +0200 Subject: [PATCH 115/192] d-4044/update: back btn --- .../NavigationBackButton.tsx | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx index 90487be8d..63160803c 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx @@ -15,35 +15,34 @@ const NavigationBack = () => { const dispatch = useDispatch(); const history = useHistory(); const location = useLocation(); - const { links, referer } = useSelector( - (state: RootState) => state.dataExplorer - ); + const { referer } = useSelector((state: RootState) => state.dataExplorer); - const onBack = () => { - if (referer?.pathname && !links.length) { - dispatch(ResetDataExplorerGraphFlow({ initialState: null })); - localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); - history.push(`${referer.pathname}${referer.search}`, { - ...referer.state, - }); - return; - } - history.replace(location.pathname); - dispatch(ReturnBackDataExplorerGraphFlow()); - }; + // const onBack = () => { + // if (referer?.pathname && !links.length) { + // dispatch(ResetDataExplorerGraphFlow({ initialState: null })); + // localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + // history.push(`${referer.pathname}${referer.search}`, { + // ...referer.state, + // }); + // return; + // } + // history.replace(location.pathname); + // dispatch(ReturnBackDataExplorerGraphFlow()); + // }; - return links.length || !!referer?.pathname ? ( - - ) : null; + // return links.length || !!referer?.pathname ? ( + // + // ) : null; + return null; }; export default NavigationBack; From e7789a9534ca7f9a667329a8158fcca15d41a2bc Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:14:45 +0200 Subject: [PATCH 116/192] d-4044/update: update navigation stack tests --- .../NavigationStack.spec.tsx | 128 +++++++++--------- 1 file changed, 62 insertions(+), 66 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index a035a80c7..cb9238e1d 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -10,23 +10,22 @@ import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import { NexusProvider } from '@bbp/react-nexus'; import { deltaPath } from '../../../__mocks__/handlers/handlers'; -import NavigationStack from './NavigationStack'; -import { - NavigationBackButton, - NavigationCollapseButton, -} from '../../molecules/DataExplorerGraphFlowMolecules'; import configureStore from '../../store'; import { AddNewNodeDataExplorerGraphFlow, ResetDataExplorerGraphFlow, PopulateDataExplorerGraphFlow, + TDataExplorerState, + ExpandNavigationStackDataExplorerGraphFlow, + ShrinkNavigationStackDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; +import NavigationStack from './NavigationStack'; const sampleDigest = - 'eyJjdXJyZW50Ijp7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6dm9sdW1ldHJpY2RhdGFsYXllci9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLCJ0aXRsZSI6IkJCUCBNb3VzZSBCcmFpbiBUZW1wbGF0ZSBWb2x1bWUsIDI1bSIsInR5cGVzIjpbIlZvbHVtZXRyaWNEYXRhTGF5ZXIiLCJCcmFpblRlbXBsYXRlRGF0YUxheWVyIiwiRGF0YXNldCJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLDJdfSwibGlua3MiOlt7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2xubWNlL2RhdGFzaGFwZXM6ZGF0YXNldC90cmFjZXMlMkY0NjBiZmEyZS1jYjdkLTQ0MjAtYTQ0OC0yMDMwYTZiZjRhZTQiLCJ0aXRsZSI6IjAwMV8xNDEyMTZfQTFfQ0ExcHlfUl9NUEciLCJ0eXBlcyI6WyJFbnRpdHkiLCJUcmFjZSIsIkRhdGFzZXQiXSwicmVzb3VyY2UiOlsiYmJwIiwibG5tY2UiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvdHJhY2VzLzQ2MGJmYTJlLWNiN2QtNDQyMC1hNDQ4LTIwMzBhNmJmNGFlNCIsMTVdfSx7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6YXRsYXNzcGF0aWFscmVmZXJlbmNlc3lzdGVtL2FsbGVuX2NjZnYzX3NwYXRpYWxfcmVmZXJlbmNlX3N5c3RlbSIsInRpdGxlIjoiQWxsZW4gTW91c2UgQ0NGIiwidHlwZXMiOlsiQXRsYXNTcGF0aWFsUmVmZXJlbmNlU3lzdGVtIiwiQnJhaW5BdGxhc1NwYXRpYWxSZWZlcmVuY2VTeXN0ZW0iXSwicmVzb3VyY2UiOlsiYmJwIiwiYXRsYXMiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvYWxsZW5fY2NmdjNfc3BhdGlhbF9yZWZlcmVuY2Vfc3lzdGVtIiw5XX1dLCJzaHJpbmtlZCI6ZmFsc2UsImhpZ2hsaWdodEluZGV4IjotMX0='; + 'eyJjdXJyZW50Ijp7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvYXRsYXMvZGF0YXNoYXBlczp2b2x1bWV0cmljZGF0YWxheWVyL2RjYTQwZjk5LWI0OTQtNGQyYy05YTJmLWM0MDcxODAxMzhiNyIsInRpdGxlIjoiYXZlcmFnZV90ZW1wbGF0ZV8yNSIsInR5cGVzIjpbIkJyYWluVGVtcGxhdGVEYXRhTGF5ZXIiLCJWb2x1bWV0cmljRGF0YUxheWVyIl0sInJlc291cmNlIjpbImJicCIsImF0bGFzIiwiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXVyb3NjaWVuY2VncmFwaC9kYXRhL2RjYTQwZjk5LWI0OTQtNGQyYy05YTJmLWM0MDcxODAxMzhiNyIsOF19LCJsZWZ0Tm9kZXMiOnsibGlua3MiOlt7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvbG5tY2UvZGF0YXNoYXBlczpkYXRhc2V0L3RyYWNlcyUyRjYwOTBiNWZlLTU0YmYtNDRiNC1hZjU5LWE0NmE5YmI4MWZhYyIsInRpdGxlIjoiMDAxXzE1MDMwNF9BMl9DQTFweV9NUEciLCJ0eXBlcyI6WyJUcmFjZSIsIkRhdGFzZXQiLCJFbnRpdHkiXSwicmVzb3VyY2UiOlsiYmJwIiwibG5tY2UiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvdHJhY2VzLzYwOTBiNWZlLTU0YmYtNDRiNC1hZjU5LWE0NmE5YmI4MWZhYyIsMTddfSx7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvYXRsYXMvZGF0YXNoYXBlczphdGxhc3JlbGVhc2UvODMxYTYyNmEtYzBhZS00NjkxLThjZTgtY2ZiNzQ5MTM0NWQ5IiwidGl0bGUiOiJBbGxlbiBNb3VzZSBDQ0YgdjMiLCJ0eXBlcyI6WyJCcmFpbkF0bGFzUmVsZWFzZSIsIkF0bGFzUmVsZWFzZSJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS84MzFhNjI2YS1jMGFlLTQ2OTEtOGNlOC1jZmI3NDkxMzQ1ZDkiLDRdfV0sInNocmlua2VkIjpmYWxzZX0sInJpZ2h0Tm9kZXMiOnsibGlua3MiOlt7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvYXRsYXMvZGF0YXNoYXBlczphdGxhc3NwYXRpYWxyZWZlcmVuY2VzeXN0ZW0vYWxsZW5fY2NmdjNfc3BhdGlhbF9yZWZlcmVuY2Vfc3lzdGVtIiwidGl0bGUiOiJBbGxlbiBNb3VzZSBDQ0YiLCJ0eXBlcyI6WyJBdGxhc1NwYXRpYWxSZWZlcmVuY2VTeXN0ZW0iLCJCcmFpbkF0bGFzU3BhdGlhbFJlZmVyZW5jZVN5c3RlbSJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS9hbGxlbl9jY2Z2M19zcGF0aWFsX3JlZmVyZW5jZV9zeXN0ZW0iLDldfSx7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvYXRsYXMvZGF0YXNoYXBlczpvcmdhbml6YXRpb24vaHR0cHM6JTJGJTJGd3d3LmdyaWQuYWMlMkZpbnN0aXR1dGVzJTJGZ3JpZC40MTc4ODEuMyIsInRpdGxlIjoiQWxsZW4gSW5zdGl0dXRlIGZvciBCcmFpbiBTY2llbmNlIiwidHlwZXMiOlsiQWdlbnQiLCJPcmdhbml6YXRpb24iXSwicmVzb3VyY2UiOlsiYmJwIiwiYXRsYXMiLCJodHRwczovL3d3dy5ncmlkLmFjL2luc3RpdHV0ZXMvZ3JpZC40MTc4ODEuMyIsMV19XSwic2hyaW5rZWQiOmZhbHNlfSwicmVmZXJlciI6eyJwYXRobmFtZSI6Ii9iYnAvbG5tY2UvcmVzb3VyY2VzL2h0dHBzJTNBJTJGJTJGYmJwLmVwZmwuY2glMkZuZXVyb3NjaWVuY2VncmFwaCUyRmRhdGElMkZ0cmFjZXMlMkY2MDkwYjVmZS01NGJmLTQ0YjQtYWY1OS1hNDZhOWJiODFmYWMiLCJzZWFyY2giOiIiLCJzdGF0ZSI6eyJiYWNrZ3JvdW5kIjp7InBhdGhuYW1lIjoiL3NlYXJjaCIsInNlYXJjaCI6Ij9sYXlvdXQ9TmV1cm9uIEVsZWN0cm9waHlzaW9sb2d5IiwiaGFzaCI6IiIsImtleSI6ImdvNnNhZCJ9fX19'; const digestCurrentSelf = - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:volumetricdatalayer/cce06e96-85a4-403d-8b87-634a1554cdb9'; -const initialDataExplorerState = { + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:volumetricdatalayer/dca40f99-b494-4d2c-9a2f-c407180138b7'; +const initialDataExplorerState: TDataExplorerState = { current: { isDownloadable: false, _self: @@ -40,34 +39,40 @@ const initialDataExplorerState = { 1, ], }, - links: [ - { - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', - title: '001_141216_A1_CA1py_R_MPG', - types: ['Entity', 'Trace', 'Dataset'], - resource: [ - 'bbp', - 'lnmce', - 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', - ], - }, - { - isDownloadable: false, - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', - title: 'Ecole Polytechnique Federale de Lausanne', - types: ['Organization', 'prov#Agent'], - resource: [ - 'bbp', - 'atlas', - 'https://www.grid.ac/institutes/grid.5333.6', - 1, - ], - }, - ], - shrinked: false, - highlightIndex: -1, + leftNodes: { + links: [ + { + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + title: '001_141216_A1_CA1py_R_MPG', + types: ['Entity', 'Trace', 'Dataset'], + resource: [ + 'bbp', + 'lnmce', + 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + 1, + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', + title: 'Ecole Polytechnique Federale de Lausanne', + types: ['Organization', 'prov#Agent'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.5333.6', + 1, + ], + }, + ], + shrinked: false, + }, + rightNodes: { + links: [], + shrinked: false, + }, limited: false, }; const fourthItemInStack = { @@ -106,9 +111,8 @@ describe('NavigationStack', () => { <> - - - + + @@ -130,18 +134,6 @@ describe('NavigationStack', () => { ); expect(navigationItems.length).toBe(2); }); - it('should render the correct number of NavigationStackItem components after hit the return back btn', async () => { - store.dispatch( - ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) - ); - const navigationBackItem = container.querySelector('.navigation-back-btn'); - navigationBackItem && (await user.click(navigationBackItem)); - rerender(app); - const navigationItemsAfterBack = container.querySelectorAll( - '.navigation-stack-item:not(.no-more)' - ); - expect(navigationItemsAfterBack.length).toBe(1); - }); it('should render the correct number of NavigationStackItem after multiple navigation', () => { store.dispatch( ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) @@ -174,11 +166,11 @@ describe('NavigationStack', () => { store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); rerender(app); const navigationItemsAfterMultipleNavigation = container.querySelectorAll( - '.navigation-stack-item:not(.no-more)' + '.navigation-stack-item:not(.more)' ); expect(navigationItemsAfterMultipleNavigation.length).toBe(5); const state = store.getState(); - expect(state.dataExplorer.links.length).toBe(5); + expect(state.dataExplorer.leftNodes.links.length).toBe(5); }); it('should render the NavigationStackShrinkedItem when it passed MAX_NAVIGATION_ITEMS_IN_STACK', () => { store.dispatch( @@ -255,7 +247,6 @@ describe('NavigationStack', () => { store.dispatch( ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) ); - rerender(app); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -320,10 +311,10 @@ describe('NavigationStack', () => { expect(openShrinkedNavList).toBeInTheDocument(); openShrinkedNavList && (await user.click(openShrinkedNavList)); rerender(app); - const collapseBtn = container.querySelector('.navigation-collapse-btn'); + const collapseBtn = container.querySelector('.collapse-btn'); expect(collapseBtn).not.toBeNull(); expect(collapseBtn).toBeInTheDocument(); - expect(store.getState().dataExplorer.shrinked).toBe(false); + expect(store.getState().dataExplorer.leftNodes.shrinked).toBe(false); }); it('should the items in the stack be 4 when the user jump to the 5th item', async () => { store.dispatch( @@ -386,17 +377,22 @@ describe('NavigationStack', () => { }) ); rerender(app); - const forthNode = container.querySelector( - '.navigation-stack-item.item-4 > .anticon.anticon-plus' + // 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"]' ); - expect(forthNode).not.toBeNull(); - expect(forthNode).toBeInTheDocument(); - forthNode && (await user.click(forthNode)); + expect(forthNodeNavigationItem).not.toBeNull(); + expect(forthNodeNavigationItem).toBeInTheDocument(); + forthNodeNavigationItem && (await user.click(forthNodeNavigationItem)); rerender(app); - const navigationItems = container.querySelectorAll( - '.navigation-stack-item:not(.no-more)' + const navigationLeftItems = container.querySelectorAll( + '.navigation-stack-item.left:not(.more)' + ); + expect(navigationLeftItems.length).toBe(4); + const navigationRightItems = container.querySelectorAll( + '.navigation-stack-item.right:not(.more)' ); - expect(navigationItems.length).toBe(4); + expect(navigationRightItems.length).toBe(3); const state = store.getState(); expect(state.dataExplorer.current._self).toEqual(fourthItemInStack._self); }); @@ -405,8 +401,8 @@ describe('NavigationStack', () => { store.dispatch(PopulateDataExplorerGraphFlow(sampleDigest)); rerender(app); const dataExplorerState = store.getState().dataExplorer; - expect(dataExplorerState.links.length).toBe(2); + expect(dataExplorerState.leftNodes.links.length).toBe(2); + expect(dataExplorerState.rightNodes.links.length).toBe(2); expect(dataExplorerState.current._self).toBe(digestCurrentSelf); - expect(dataExplorerState.shrinked).toBe(false); }); }); From 321977079941ff57b370240b55449ee33a0f1801 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:27:49 +0200 Subject: [PATCH 117/192] d-4044/update: update de-gf tests --- .../DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx index 8f48c74d5..9d812189b 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx @@ -124,12 +124,13 @@ describe('DataExplorerGraphFlow', () => { ); history.push('/another-page'); const dataExplorerState = store.getState().dataExplorer; - const localstorage = localStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + const localstorage = sessionStorage.getItem( + DATA_EXPLORER_GRAPH_FLOW_DIGEST + ); expect(localstorage).toBeNull(); - expect(dataExplorerState.links.length).toBe(0); + expect(dataExplorerState.leftNodes.links.length).toBe(0); + expect(dataExplorerState.rightNodes.links.length).toBe(0); expect(dataExplorerState.current).toBeNull(); - expect(dataExplorerState.shrinked).toBe(false); expect(dataExplorerState.limited).toBe(false); - expect(dataExplorerState.highlightIndex).toBe(-1); }); }); From 8cf14faaac9b6cf57c5667ff4e83a3c6b14d1a9d Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:34:32 +0200 Subject: [PATCH 118/192] d-4044/update: update editor bg --- src/shared/components/ResourceEditor/ResourceEditor.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index a7faf3755..35fc6bc3b 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -29,7 +29,7 @@ } .cm-s-base16-light.CodeMirror { - background-color: white; + background-color: #f5f5f5 !important; } .resource-editor { From 1a1f55abc46eb4691baf1e2e178b69e95bb78810 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:08:57 +0200 Subject: [PATCH 119/192] d-4044/update: add new data explorer gf state structure and types --- src/server/index.tsx | 5 +- src/shared/store/reducers/data-explorer.ts | 267 +++++++++++++++------ 2 files changed, 198 insertions(+), 74 deletions(-) diff --git a/src/server/index.tsx b/src/server/index.tsx index a18dbcf7d..379d42283 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -148,10 +148,9 @@ app.get('*', async (req: express.Request, res: express.Response) => { modals: DEFAULT_MODALS_STATE, dataExplorer: { current: null, - links: [], - shrinked: false, + leftNodes: { links: [], shrinked: false }, + rightNodes: { links: [], shrinked: false }, limited: false, - highlightIndex: -1, }, }; diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index f4553ec65..8856d169f 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -1,5 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; -import { slice, omit, clone, dropRight, nth } from 'lodash'; +import { slice, clone, dropRight, nth, last, concat } from 'lodash'; type TProject = string; type TOrganization = string; @@ -7,9 +7,20 @@ type TResourceID = string; type TVersionTag = number; type TMediaType = string; -export type TDEResource = - | [TOrganization, TProject, TResourceID, TVersionTag] - | [TOrganization, TProject, TResourceID, TVersionTag, TMediaType]; +export type TDEResourceWithoutMedia = [ + TOrganization, + TProject, + TResourceID, + TVersionTag +]; +export type TDEResourceWithMedia = [ + TOrganization, + TProject, + TResourceID, + TVersionTag, + TMediaType +]; +export type TDEResource = TDEResourceWithoutMedia | TDEResourceWithMedia; export type TDELink = { _self: string; @@ -18,46 +29,50 @@ export type TDELink = { resource?: TDEResource; isDownloadable?: boolean; }; + export type TDataExplorerState = { - links: TDELink[]; + leftNodes: { + links: TDELink[]; + shrinked: boolean; + }; + rightNodes: { + links: TDELink[]; + shrinked: boolean; + }; current: TDELink | null; referer?: { pathname: string; search: string; state: Record; } | null; - shrinked: boolean; limited: boolean; - highlightIndex: number; }; +export type TNavigationStackSide = 'left' | 'right'; +export const DATA_EXPLORER_GRAPH_FLOW_PATH = '/data-explorer/graph-flow'; +export const DATA_EXPLORER_GRAPH_FLOW_DIGEST = 'data-explorer-last-navigation'; +export const MAX_NAVIGATION_ITEMS_IN_STACK = 3; + const initialState: TDataExplorerState = { - links: [], + leftNodes: { links: [], shrinked: false }, + rightNodes: { links: [], shrinked: false }, current: null, referer: null, - shrinked: false, limited: false, - highlightIndex: -1, }; -export const DATA_EXPLORER_GRAPH_FLOW_PATH = '/data-explorer/graph-flow'; -export const DATA_EXPLORER_GRAPH_FLOW_DIGEST = 'data-explorer-last-navigation'; -export const MAX_NAVIGATION_ITEMS_IN_STACK = 5; const calculateNewDigest = (state: TDataExplorerState) => { const clonedState = clone(state); - const digest = btoa( - JSON.stringify({ - ...clonedState, - links: clonedState.links.map(i => omit(i, ['highlight'])), - current: omit(clonedState.current, ['highlight']), - }) - ); + const digest = btoa(JSON.stringify(clonedState)); sessionStorage.setItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST, digest); }; const isShrinkable = (links: TDELink[]) => { return links.length > MAX_NAVIGATION_ITEMS_IN_STACK; }; +function insert(arr: any[], index: number, item: any) { + arr.splice(index, 0, item); +} export const dataExplorerSlice = createSlice({ initialState, @@ -69,7 +84,14 @@ export const dataExplorerSlice = createSlice({ try { return { ...newState, - shrinked: isShrinkable(newState.links), + leftNodes: { + links: newState.leftNodes.links, + shrinked: isShrinkable(newState.leftNodes.links), + }, + rightNodes: { + links: newState.rightNodes.links, + shrinked: isShrinkable(newState.rightNodes.links), + }, }; } catch (error) { return state; @@ -84,88 +106,192 @@ export const dataExplorerSlice = createSlice({ referer, current, limited, - links: - source && current - ? source._self === current._self - ? [] - : [source] - : [], + leftNodes: { + links: + source && current + ? source._self === current._self + ? [] + : [source] + : [], + shrinked: false, + }, }; calculateNewDigest(newState); return newState; }, AddNewNodeDataExplorerGraphFlow: (state, action) => { - const linkIndex = state.links.findIndex( - item => item._self === action.payload._self - ); - const isCurrentLink = state.current?._self === action.payload._self; - const newLinks = isCurrentLink - ? state.links - : linkIndex !== -1 - ? state.links - : [...state.links, state.current!]; - const newCurrent = - isCurrentLink || linkIndex !== -1 ? state.current : action.payload; - const newState: TDataExplorerState = { + if (action.payload._self === state.current?._self) { + console.log('@@same node'); + return state; + } + const newLink = action.payload as TDELink; + const whichSide = state.leftNodes.links.find( + link => link._self === newLink._self + ) + ? 'left' + : state.rightNodes.links.find(link => link._self === newLink._self) + ? 'right' + : null; + let leftNodesLinks: TDELink[] = []; + let rightNodesLinks: TDELink[] = []; + let current: TDELink; + switch (whichSide) { + case 'left': { + const index = state.leftNodes.links.findIndex( + link => link._self === newLink._self + ); + rightNodesLinks = concat( + slice(state.leftNodes.links, index + 1), + state.current ? [state.current] : [], + state.rightNodes.links + ); + leftNodesLinks = slice(state.leftNodes.links, 0, index); + current = state.leftNodes.links[index]; + break; + } + case 'right': { + const index = state.rightNodes.links.findIndex( + link => link._self === newLink._self + ); + // make the new link the current one + // add the links before that and the current one the left part + leftNodesLinks = concat( + state.leftNodes.links, + state.current ? [state.current] : [], + slice(state.rightNodes.links, 0, index) + ); + rightNodesLinks = slice(state.rightNodes.links, index + 1); + current = state.rightNodes.links[index]; + break; + } + case null: + default: { + leftNodesLinks = concat( + state.leftNodes.links, + state.current ? [state.current] : [] + ); + rightNodesLinks = []; + current = action.payload; + break; + } + } + const newState = { ...state, - links: newLinks, - current: newCurrent, - highlightIndex: linkIndex, - shrinked: linkIndex !== -1 ? false : isShrinkable(newLinks), + current, + leftNodes: { + links: leftNodesLinks, + shrinked: isShrinkable(leftNodesLinks), + }, + rightNodes: { + links: rightNodesLinks, + shrinked: isShrinkable(rightNodesLinks), + }, }; calculateNewDigest(newState); return newState; }, JumpToNodeDataExplorerGraphFlow: (state, action) => { - const newLinks = slice(state.links, 0, action.payload); - const newState: TDataExplorerState = { + const index = action.payload.index as number; + const side = action.payload.side as TNavigationStackSide; + const realIndex = + side === 'left' ? index : state.leftNodes.links.length + index + 1; + const allLinks = concat( + state.leftNodes.links, + state.current ? [state.current] : [], + state.rightNodes.links + ); + const current = nth(allLinks, realIndex) as TDELink; + // construct left part + const leftNodesLinks = slice(allLinks, 0, realIndex); + const leftNodes = { + links: leftNodesLinks, + shrinked: isShrinkable(leftNodesLinks), + }; + // construct right part + const rightNodesLinks = slice(allLinks, realIndex + 1); + const rightNodes = { + links: rightNodesLinks, + shrinked: isShrinkable(rightNodesLinks), + }; + const newState = { ...state, - links: newLinks, - current: state.links[action.payload], - shrinked: isShrinkable(newLinks), + leftNodes, + rightNodes, + current, }; calculateNewDigest(newState); return newState; }, ReturnBackDataExplorerGraphFlow: state => { - const lastItem = state.links.length ? nth(state.links, -1) : null; - const newLinks = dropRight(state.links); - const newState = lastItem - ? { - ...state, - links: newLinks, - current: lastItem, - shrinked: isShrinkable(newLinks), - } - : state; + const current = last(state.leftNodes.links) as TDELink; + const newrightNodesLinks = state.rightNodes.links; + const newleftNodesLinks = dropRight(state.leftNodes.links) as TDELink[]; + insert(newrightNodesLinks, 0, state.current); + const rightNodes = { + links: newrightNodesLinks, + shrinked: isShrinkable(newrightNodesLinks), + }; + const newState = { + ...state, + current, + rightNodes, + leftNodes: { + links: newleftNodesLinks, + shrinked: isShrinkable(newleftNodesLinks), + }, + }; calculateNewDigest(newState); return newState; }, - ExpandNavigationStackDataExplorerGraphFlow: state => { + ExpandNavigationStackDataExplorerGraphFlow: (state, action) => { + const side = action.payload.side as TNavigationStackSide; + const sideUpdater = + side === 'left' + ? { + leftNodes: { + ...state.leftNodes, + shrinked: false, + }, + } + : { + rightNodes: { + ...state.rightNodes, + shrinked: false, + }, + }; const newState = { ...state, - shrinked: false, + ...sideUpdater, }; calculateNewDigest(newState); return newState; }, - ShrinkNavigationStackDataExplorerGraphFlow: state => { + ShrinkNavigationStackDataExplorerGraphFlow: (state, action) => { + const side = action.payload.side as TNavigationStackSide; + const sideUpdater = + side === 'left' + ? { + leftNodes: { + ...state.leftNodes, + shrinked: isShrinkable(state.leftNodes.links) ? true : false, + }, + } + : { + rightNodes: { + ...state.rightNodes, + shrinked: isShrinkable(state.rightNodes.links) ? true : false, + }, + }; const newState = { ...state, - shrinked: isShrinkable(state.links) ? true : false, + ...sideUpdater, }; calculateNewDigest(newState); return newState; }, - ResetDataExplorerGraphFlow: (state, action) => { + ResetDataExplorerGraphFlow: (_, action) => { return action.payload.initialState ?? initialState; }, - ResetHighlightedNodeDataExplorerGraphFlow: state => { - return { - ...state, - highlightIndex: -1, - }; - }, InitDataExplorerGraphFlowLimitedVersion: (state, action) => { const newState = { ...state, @@ -180,12 +306,11 @@ export const { PopulateDataExplorerGraphFlow, InitNewVisitDataExplorerGraphView, AddNewNodeDataExplorerGraphFlow, - JumpToNodeDataExplorerGraphFlow, ExpandNavigationStackDataExplorerGraphFlow, ShrinkNavigationStackDataExplorerGraphFlow, + JumpToNodeDataExplorerGraphFlow, ReturnBackDataExplorerGraphFlow, ResetDataExplorerGraphFlow, - ResetHighlightedNodeDataExplorerGraphFlow, InitDataExplorerGraphFlowLimitedVersion, } = dataExplorerSlice.actions; From a8bc39eafeb64ecac70ae4d98aadb44d242509f9 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:09:41 +0200 Subject: [PATCH 120/192] d-4044/update: update navigation collapse btn for both sides --- src/shared/components/Icons/Collapse.tsx | 20 ++++++++++ .../NavigationCollapseButton.tsx | 39 +++++++++---------- 2 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 src/shared/components/Icons/Collapse.tsx diff --git a/src/shared/components/Icons/Collapse.tsx b/src/shared/components/Icons/Collapse.tsx new file mode 100644 index 000000000..34f92b90e --- /dev/null +++ b/src/shared/components/Icons/Collapse.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +const CollapseIcon = (props: React.SVGProps) => { + return ( + + + + ); +}; + +export default CollapseIcon; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx index 3fe021786..a638556dc 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationCollapseButton.tsx @@ -1,24 +1,23 @@ -import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { ShrinkOutlined } from '@ant-design/icons'; -import { RootState } from '../../store/reducers'; -import { - MAX_NAVIGATION_ITEMS_IN_STACK, - ShrinkNavigationStackDataExplorerGraphFlow, -} from '../../store/reducers/data-explorer'; -import './styles.less'; +import * as React from 'react'; +import { clsx } from 'clsx'; +import { Tooltip } from 'antd'; +import CollapseIcon from '../../components/Icons/Collapse'; +import { TNavigationStackSide } from '../../store/reducers/data-explorer'; -const NavigationHamburguer = () => { - const { shrinked, links } = useSelector( - (state: RootState) => state.dataExplorer +const NavigationCollapseButton = ({ + side, + onExpand, +}: { + side: TNavigationStackSide; + onExpand: () => void; +}) => { + return ( + + + ); - const dispatch = useDispatch(); - const onShrink = () => dispatch(ShrinkNavigationStackDataExplorerGraphFlow()); - return !shrinked && links.length > MAX_NAVIGATION_ITEMS_IN_STACK ? ( - - ) : null; }; -export default NavigationHamburguer; +export default NavigationCollapseButton; From 1da2d3db4281d9f05672c8cab46335b4ecec386d Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:10:23 +0200 Subject: [PATCH 121/192] d-4044/update: update navigation btns (simple/shrinked) and styles --- .../NavigationStackItem.tsx | 118 +++++++--- .../NavigationStackShrinkedItem.tsx | 53 ++--- .../styles.less | 204 +++++++++++------- .../NavigationStack.tsx | 71 +++--- .../styles.less | 7 +- .../useNavigationStack.ts | 40 ++++ 6 files changed, 327 insertions(+), 166 deletions(-) create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx index 7b35bf562..47e5e10a2 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackItem.tsx @@ -1,15 +1,19 @@ -import React from 'react'; +import * as React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router'; -import { Tag, Tooltip } from 'antd'; +import { Space, Tag, Tooltip } from 'antd'; import { clsx } from 'clsx'; import { isArray } from 'lodash'; -import { PlusOutlined } from '@ant-design/icons'; +import { FullscreenOutlined } from '@ant-design/icons'; import { TDEResource, JumpToNodeDataExplorerGraphFlow, + TNavigationStackSide, + MAX_NAVIGATION_ITEMS_IN_STACK, } from '../../store/reducers/data-explorer'; import { RootState } from '../../store/reducers'; +import useNavigationStackManager from '../../organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack'; +import NavigationCollapseButton from './NavigationCollapseButton'; import './styles.less'; export type TNavigationStackItem = { @@ -18,7 +22,7 @@ export type TNavigationStackItem = { types?: string | string[]; title: string; resource?: TDEResource; - highlighted: boolean; + side: TNavigationStackSide; }; const NavigationStackItem = ({ @@ -27,46 +31,108 @@ const NavigationStackItem = ({ title, types, resource, - highlighted, + side, }: TNavigationStackItem) => { const dispatch = useDispatch(); const location = useLocation(); const history = useHistory(); - const { shrinked, links } = useSelector( + const { leftNodes, rightNodes } = useSelector( (state: RootState) => state.dataExplorer ); + const { + onRightShrink, + onLeftShrink, + leftShrinked, + rightShrinked, + } = useNavigationStackManager(); + const onClick = () => { - dispatch(JumpToNodeDataExplorerGraphFlow(index)); + dispatch(JumpToNodeDataExplorerGraphFlow({ index, side })); history.replace(location.pathname); }; + const parentNode = side === 'left' ? leftNodes : rightNodes; + const orgProject = + resource?.[0] && resource?.[1] && `${resource?.[0]}/${resource?.[1]}`; + const showLeftCollapseBtn = + side === 'left' && + !leftShrinked && + leftNodes.links.length > MAX_NAVIGATION_ITEMS_IN_STACK && + leftNodes.links.length - 1 === index; + const showRightCollapseBtn = + side === 'right' && + !rightShrinked && + rightNodes.links.length > MAX_NAVIGATION_ITEMS_IN_STACK && + index === 0; + + const collapseRightBtn = React.useCallback(() => { + return ( + showRightCollapseBtn && ( + + ) + ); + }, [showRightCollapseBtn, side, onRightShrink]); + + const collapseLeftBtn = React.useCallback(() => { + return ( + showLeftCollapseBtn && ( + + ) + ); + }, [showLeftCollapseBtn, side, onLeftShrink]); + return ( + {collapseLeftBtn()}
); }; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx index 80c26c26f..b31d44491 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationStackShrinkedItem.tsx @@ -4,39 +4,42 @@ import { clsx } from 'clsx'; import { MoreOutlined, PlusOutlined } from '@ant-design/icons'; import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '../../store/reducers'; -import { ExpandNavigationStackDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; import './styles.less'; +import { TDELink } from 'shared/store/reducers/data-explorer'; const BORDER_ITEMS = 2; -const NavigationStackShrinkedItem = () => { - const { links, shrinked, highlightIndex } = useSelector( - (state: RootState) => state.dataExplorer - ); - const dispatch = useDispatch(); +const NavigationStackShrinkedItem = ({ + side, + links, + shrinked, + onExpand, +}: { + side: 'left' | 'right'; + shrinked: boolean; + links: TDELink[]; + onExpand: () => void; +}) => { const count = links.length - BORDER_ITEMS; - const onClick = () => dispatch(ExpandNavigationStackDataExplorerGraphFlow()); return ( } - > - - -
{count}
- +
+ Open {count} other resources
} + > + + +
{count}
+ +
); }; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index 0342f81fa..a08136a77 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -1,26 +1,104 @@ @import '../../lib.less'; .navigation-stack-item { - display: grid; - grid-template-rows: repeat(4, max-content); - gap: 10px; - align-items: start; - justify-items: center; - justify-content: center; - align-content: start; - border-right: 1px solid @fusion-neutral-6; - padding: 3px 5px; - height: 100%; - min-height: 0; - background-color: @fusion-main-bg; + display: flex; + align-items: flex-start; - &.more { - background-color: white; + &.shrinkable { + display: none; } - &.no-more { - display: none; - transition: display 0.2s cubic-bezier(0.075, 0.82, 0.165, 1); + &__wrapper { + display: grid; + grid-template-rows: repeat(4, max-content); + gap: 10px; + align-items: start; + justify-items: center; + justify-content: center; + align-content: start; + padding: 3px 5px; + height: 100%; + min-height: 0; + background-color: @fusion-main-bg; + position: relative; + + &.more { + background-color: white; + } + + &.no-more { + display: none; + transition: display 0.2s cubic-bezier(0.075, 0.82, 0.165, 1); + } + + .icon { + margin-top: 5px !important; + padding: 4px !important; + } + + .title { + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 600; + font-size: 12px; + line-height: 130%; + letter-spacing: 0.01em; + color: #003a8c; + writing-mode: vertical-lr; + text-orientation: sideways-right; + transform: rotate(180deg); + align-self: start; + } + + .org-project { + writing-mode: vertical-rl; + transform: rotate(-180deg); + background-color: #bfbfbfbe; + padding: 5px 1px; + font-size: 10px; + border-radius: 4px; + user-select: none; + } + + .types { + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 300; + font-size: 12px; + line-height: 130%; + letter-spacing: 0.01em; + color: #8c8c8c; + writing-mode: vertical-lr; + text-orientation: mixed; + align-self: start; + transform: rotate(180deg); + } + + .count { + font-weight: 600; + font-size: 15px; + line-height: 130%; + letter-spacing: 0.01em; + color: #bfbfbf; + } + + .ellipsis { + user-select: none; + writing-mode: vertical-lr; + text-orientation: mixed; + } + } + + &.right { + .navigation-stack-item__wrapper { + border-left: 1px solid @fusion-neutral-6; + } + } + + &.left { + .navigation-stack-item__wrapper { + border-right: 1px solid @fusion-neutral-6; + } } &.highlight { @@ -33,65 +111,39 @@ animation: vibrate 0.3s linear infinite both; } - &.shrinkable { - display: none; - } + .collapse-btn { + inset: 0; + overflow: visible; + appearance: none; + -webkit-appearance: none; + padding: 8px 8px; + background-color: @fusion-blue-8; + display: flex; + align-items: center; + justify-content: center; + margin-top: 40px; + box-shadow: 0 2px 12px rgba(#333, 0.12); + cursor: pointer; + border: none; - .icon { - margin-top: 5px !important; - padding: 4px !important; - } - - .title { - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 600; - font-size: 12px; - line-height: 130%; - letter-spacing: 0.01em; - color: #003a8c; - writing-mode: vertical-lr; - text-orientation: sideways-right; - transform: rotate(180deg); - align-self: start; - } - - .org-project { - writing-mode: vertical-rl; - transform: rotate(-180deg); - background-color: #bfbfbfbe; - padding: 5px 1px; - font-size: 10px; - border-radius: 4px; - user-select: none; - } - - .types { - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 300; - font-size: 12px; - line-height: 130%; - letter-spacing: 0.01em; - color: #8c8c8c; - writing-mode: vertical-lr; - text-orientation: mixed; - align-self: start; - transform: rotate(180deg); - } + &.right { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + svg { + transform: rotate(180deg); + } + } - .count { - font-weight: 600; - font-size: 15px; - line-height: 130%; - letter-spacing: 0.01em; - color: #bfbfbf; - } + &.left { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } - .ellipsis { - user-select: none; - writing-mode: vertical-lr; - text-orientation: mixed; + svg { + color: white; + width: 16px; + height: 16px; + } } } @@ -130,6 +182,7 @@ color: @fusion-daybreak-7; } } + &.go-back-to-referer { margin-left: 40px; } @@ -192,22 +245,27 @@ -webkit-transform: translate(0); transform: translate(0); } + 20% { -webkit-transform: translate(-1.5px, 1.5px); transform: translate(-1.5px, 1.5px); } + 40% { -webkit-transform: translate(-1.5px, -1.5px); transform: translate(-1.5px, -1.5px); } + 60% { -webkit-transform: translate(1.5px, 1.5px); transform: translate(1.5px, 1.5px); } + 80% { -webkit-transform: translate(1.5px, -1.5px); transform: translate(1.5px, -1.5px); } + 100% { -webkit-transform: translate(0); transform: translate(0); diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx index 70ed128e7..95b679a46 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.tsx @@ -1,78 +1,67 @@ -import React, { Fragment, useEffect, useRef } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import React, { Fragment } from 'react'; import { clsx } from 'clsx'; -import { RootState } from '../../store/reducers'; -import { ResetHighlightedNodeDataExplorerGraphFlow } from '../../store/reducers/data-explorer'; +import { TNavigationStackSide } from '../../store/reducers/data-explorer'; import { NavigationStackItem, NavigationStackShrinkedItem, } from '../../molecules/DataExplorerGraphFlowMolecules'; +import useNavigationStackManager from './useNavigationStack'; import './styles.less'; -import { animate, spring } from 'motion'; -const NavigationStack = () => { - const dispatch = useDispatch(); - const { links, shrinked, highlightIndex } = useSelector( - (state: RootState) => state.dataExplorer - ); - const shrinkedRef = useRef(shrinked); - const stackRef = useRef(null); - - if (highlightIndex !== -1) { - setTimeout(() => { - dispatch(ResetHighlightedNodeDataExplorerGraphFlow()); - }, 2000); - } - useEffect(() => { - if (shrinkedRef.current && !shrinked && stackRef.current) { - animate( - stackRef.current, - { scale: 1 }, - { easing: spring({ stiffness: 10, damping: 10, mass: 500 }) } - ); - } else if (stackRef.current) { - animate( - stackRef.current, - { display: 'grid' }, - { easing: spring({ stiffness: 300, damping: 10 }) } - ); - } - }, [shrinked, shrinkedRef.current]); +const NavigationStack = ({ side }: { side: TNavigationStackSide }) => { + const { + leftLinks, + rightLinks, + leftShrinked, + rightShrinked, + onRightExpand, + onLeftExpand, + } = useNavigationStackManager(); + const links = side === 'left' ? leftLinks : rightLinks; + const shrinked = side === 'left' ? leftShrinked : rightShrinked; + const onExpand = side === 'left' ? onLeftExpand : onRightExpand; return ( -
-
+
+
{links.map(({ title, types, _self, resource }, index) => { if (index === 0) { return ( - + {shrinked && ( + + )} ); } return ( ); diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less index 98ac85b59..5575a9b71 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less @@ -3,9 +3,14 @@ align-items: flex-start; position: fixed; gap: 10px; - left: 0; top: 52px; z-index: 6; + &.left { + left: 0; + } + &.right { + right: 0; + } } .navigation-stack { diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts new file mode 100644 index 000000000..d37d1e06b --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts @@ -0,0 +1,40 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory, useLocation } from 'react-router'; +import { RootState } from '../../store/reducers'; +import { + ExpandNavigationStackDataExplorerGraphFlow, + ShrinkNavigationStackDataExplorerGraphFlow, +} from '../../store/reducers/data-explorer'; + +const useNavigationStackManager = () => { + const dispatch = useDispatch(); + const { rightNodes, leftNodes } = useSelector( + (state: RootState) => state.dataExplorer + ); + const leftShrinked = leftNodes.shrinked; + const rightShrinked = rightNodes.shrinked; + const leftLinks = leftNodes.links; + const rightLinks = rightNodes.links; + + const onLeftShrink = () => + dispatch(ShrinkNavigationStackDataExplorerGraphFlow({ side: 'left' })); + const onLeftExpand = () => + dispatch(ExpandNavigationStackDataExplorerGraphFlow({ side: 'left' })); + const onRightShrink = () => + dispatch(ShrinkNavigationStackDataExplorerGraphFlow({ side: 'right' })); + const onRightExpand = () => + dispatch(ExpandNavigationStackDataExplorerGraphFlow({ side: 'right' })); + + return { + leftShrinked, + rightShrinked, + leftLinks, + rightLinks, + onLeftShrink, + onLeftExpand, + onRightShrink, + onRightExpand, + }; +}; + +export default useNavigationStackManager; From adc531dd9aaaea55eef54aeeda7010efc8a4f7dd Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:11:11 +0200 Subject: [PATCH 122/192] d-4044/update: the de-gf page structure --- .../DateExplorerGraphFlow.tsx | 47 +++++++++++++------ .../canvas/DataExplorerGraphFlow/styles.less | 31 ++++++------ 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index 02a8fa33c..5413a260b 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -9,13 +9,10 @@ import { PopulateDataExplorerGraphFlow, ResetDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; -import { - NavigationBackButton, - NavigationCollapseButton, -} from '../../molecules/DataExplorerGraphFlowMolecules'; import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; import ResourceResolutionCache from '../../components/ResourceEditor/ResourcesLRUCache'; +import useNavigationStackManager from '../../organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack'; import './styles.less'; @@ -24,10 +21,21 @@ const DataExplorerGraphFlow = () => { const location = useLocation(); const dispatch = useDispatch(); const digestFirstRender = useRef(false); - const { links, shrinked, current } = useSelector( + const { current, rightNodes, leftNodes } = useSelector( (state: RootState) => state.dataExplorer ); + const { + leftShrinked, + rightShrinked, + leftLinks, + rightLinks, + onLeftShrink, + onLeftExpand, + onRightShrink, + onRightExpand, + } = useNavigationStackManager(); + useEffect(() => { if (!digestFirstRender.current) { const state = sessionStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); @@ -75,25 +83,34 @@ const DataExplorerGraphFlow = () => {
-
- -
-
- - -
+ {!!leftLinks.length && ( +
+ +
+ )}
+ {!!rightLinks.length && ( +
+ +
+ )}
); }; diff --git a/src/shared/canvas/DataExplorerGraphFlow/styles.less b/src/shared/canvas/DataExplorerGraphFlow/styles.less index f2a5b0a41..e0942bc71 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/styles.less +++ b/src/shared/canvas/DataExplorerGraphFlow/styles.less @@ -3,33 +3,34 @@ .data-explorer-resolver { width: 100%; max-width: 100%; + display: grid; + grid-auto-columns: 1fr; align-items: flex-start; justify-content: flex-start; background-color: @fusion-main-bg; - display: grid; - grid-auto-columns: 1fr; gap: 10px; &.no-links { grid-template-columns: 1fr; } - &.with-links { - grid-template-columns: calc(calc(var(--links-count) * 31px) + 40px) max-content 1fr; + &.left-existed { + grid-template-columns: calc(calc(var(--left--links-count) * 31px) + 40px) 1fr; + } + &.right-existed { + grid-template-columns: 1fr calc( + calc(var(--right--links-count) * 31px) + 40px + ); + } + &.left-existed.right-existed { + grid-template-columns: calc(calc(var(--left--links-count) * 31px) + 40px) 1fr calc( + calc(var(--right--links-count) * 31px) + 40px + ); + } } - .degf__navigation-stack { width: 100%; } - - .degf__navigation-back { - position: sticky; - top: 53px; - display: flex; - align-items: center; - gap: 10px; - } - .degf__content { padding: 30px 20px 10px; display: flex; @@ -37,8 +38,8 @@ align-items: flex-start; justify-content: space-between; gap: 10px; + transition: all 0.4s ease-in-out; } - &.no-current { height: 100%; width: 100%; From 4bb4250d86cffd99388f0a0a96ac09bb8e8e40c4 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:13:43 +0200 Subject: [PATCH 123/192] d-4044/update: back btn --- .../NavigationBackButton.tsx | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx index 90487be8d..63160803c 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx @@ -15,35 +15,34 @@ const NavigationBack = () => { const dispatch = useDispatch(); const history = useHistory(); const location = useLocation(); - const { links, referer } = useSelector( - (state: RootState) => state.dataExplorer - ); + const { referer } = useSelector((state: RootState) => state.dataExplorer); - const onBack = () => { - if (referer?.pathname && !links.length) { - dispatch(ResetDataExplorerGraphFlow({ initialState: null })); - localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); - history.push(`${referer.pathname}${referer.search}`, { - ...referer.state, - }); - return; - } - history.replace(location.pathname); - dispatch(ReturnBackDataExplorerGraphFlow()); - }; + // const onBack = () => { + // if (referer?.pathname && !links.length) { + // dispatch(ResetDataExplorerGraphFlow({ initialState: null })); + // localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + // history.push(`${referer.pathname}${referer.search}`, { + // ...referer.state, + // }); + // return; + // } + // history.replace(location.pathname); + // dispatch(ReturnBackDataExplorerGraphFlow()); + // }; - return links.length || !!referer?.pathname ? ( - - ) : null; + // return links.length || !!referer?.pathname ? ( + // + // ) : null; + return null; }; export default NavigationBack; From c41741e54799d5ef2de7c086cd20c86efcee3d97 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:14:45 +0200 Subject: [PATCH 124/192] d-4044/update: update navigation stack tests --- .../NavigationStack.spec.tsx | 128 +++++++++--------- 1 file changed, 62 insertions(+), 66 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index a035a80c7..cb9238e1d 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -10,23 +10,22 @@ import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import { NexusProvider } from '@bbp/react-nexus'; import { deltaPath } from '../../../__mocks__/handlers/handlers'; -import NavigationStack from './NavigationStack'; -import { - NavigationBackButton, - NavigationCollapseButton, -} from '../../molecules/DataExplorerGraphFlowMolecules'; import configureStore from '../../store'; import { AddNewNodeDataExplorerGraphFlow, ResetDataExplorerGraphFlow, PopulateDataExplorerGraphFlow, + TDataExplorerState, + ExpandNavigationStackDataExplorerGraphFlow, + ShrinkNavigationStackDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; +import NavigationStack from './NavigationStack'; const sampleDigest = - 'eyJjdXJyZW50Ijp7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6dm9sdW1ldHJpY2RhdGFsYXllci9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLCJ0aXRsZSI6IkJCUCBNb3VzZSBCcmFpbiBUZW1wbGF0ZSBWb2x1bWUsIDI1bSIsInR5cGVzIjpbIlZvbHVtZXRyaWNEYXRhTGF5ZXIiLCJCcmFpblRlbXBsYXRlRGF0YUxheWVyIiwiRGF0YXNldCJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS9jY2UwNmU5Ni04NWE0LTQwM2QtOGI4Ny02MzRhMTU1NGNkYjkiLDJdfSwibGlua3MiOlt7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2xubWNlL2RhdGFzaGFwZXM6ZGF0YXNldC90cmFjZXMlMkY0NjBiZmEyZS1jYjdkLTQ0MjAtYTQ0OC0yMDMwYTZiZjRhZTQiLCJ0aXRsZSI6IjAwMV8xNDEyMTZfQTFfQ0ExcHlfUl9NUEciLCJ0eXBlcyI6WyJFbnRpdHkiLCJUcmFjZSIsIkRhdGFzZXQiXSwicmVzb3VyY2UiOlsiYmJwIiwibG5tY2UiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvdHJhY2VzLzQ2MGJmYTJlLWNiN2QtNDQyMC1hNDQ4LTIwMzBhNmJmNGFlNCIsMTVdfSx7Il9zZWxmIjoiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXh1cy92MS9yZXNvdXJjZXMvYmJwL2F0bGFzL2RhdGFzaGFwZXM6YXRsYXNzcGF0aWFscmVmZXJlbmNlc3lzdGVtL2FsbGVuX2NjZnYzX3NwYXRpYWxfcmVmZXJlbmNlX3N5c3RlbSIsInRpdGxlIjoiQWxsZW4gTW91c2UgQ0NGIiwidHlwZXMiOlsiQXRsYXNTcGF0aWFsUmVmZXJlbmNlU3lzdGVtIiwiQnJhaW5BdGxhc1NwYXRpYWxSZWZlcmVuY2VTeXN0ZW0iXSwicmVzb3VyY2UiOlsiYmJwIiwiYXRsYXMiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvYWxsZW5fY2NmdjNfc3BhdGlhbF9yZWZlcmVuY2Vfc3lzdGVtIiw5XX1dLCJzaHJpbmtlZCI6ZmFsc2UsImhpZ2hsaWdodEluZGV4IjotMX0='; + 'eyJjdXJyZW50Ijp7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvYXRsYXMvZGF0YXNoYXBlczp2b2x1bWV0cmljZGF0YWxheWVyL2RjYTQwZjk5LWI0OTQtNGQyYy05YTJmLWM0MDcxODAxMzhiNyIsInRpdGxlIjoiYXZlcmFnZV90ZW1wbGF0ZV8yNSIsInR5cGVzIjpbIkJyYWluVGVtcGxhdGVEYXRhTGF5ZXIiLCJWb2x1bWV0cmljRGF0YUxheWVyIl0sInJlc291cmNlIjpbImJicCIsImF0bGFzIiwiaHR0cHM6Ly9iYnAuZXBmbC5jaC9uZXVyb3NjaWVuY2VncmFwaC9kYXRhL2RjYTQwZjk5LWI0OTQtNGQyYy05YTJmLWM0MDcxODAxMzhiNyIsOF19LCJsZWZ0Tm9kZXMiOnsibGlua3MiOlt7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvbG5tY2UvZGF0YXNoYXBlczpkYXRhc2V0L3RyYWNlcyUyRjYwOTBiNWZlLTU0YmYtNDRiNC1hZjU5LWE0NmE5YmI4MWZhYyIsInRpdGxlIjoiMDAxXzE1MDMwNF9BMl9DQTFweV9NUEciLCJ0eXBlcyI6WyJUcmFjZSIsIkRhdGFzZXQiLCJFbnRpdHkiXSwicmVzb3VyY2UiOlsiYmJwIiwibG5tY2UiLCJodHRwczovL2JicC5lcGZsLmNoL25ldXJvc2NpZW5jZWdyYXBoL2RhdGEvdHJhY2VzLzYwOTBiNWZlLTU0YmYtNDRiNC1hZjU5LWE0NmE5YmI4MWZhYyIsMTddfSx7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvYXRsYXMvZGF0YXNoYXBlczphdGxhc3JlbGVhc2UvODMxYTYyNmEtYzBhZS00NjkxLThjZTgtY2ZiNzQ5MTM0NWQ5IiwidGl0bGUiOiJBbGxlbiBNb3VzZSBDQ0YgdjMiLCJ0eXBlcyI6WyJCcmFpbkF0bGFzUmVsZWFzZSIsIkF0bGFzUmVsZWFzZSJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS84MzFhNjI2YS1jMGFlLTQ2OTEtOGNlOC1jZmI3NDkxMzQ1ZDkiLDRdfV0sInNocmlua2VkIjpmYWxzZX0sInJpZ2h0Tm9kZXMiOnsibGlua3MiOlt7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvYXRsYXMvZGF0YXNoYXBlczphdGxhc3NwYXRpYWxyZWZlcmVuY2VzeXN0ZW0vYWxsZW5fY2NmdjNfc3BhdGlhbF9yZWZlcmVuY2Vfc3lzdGVtIiwidGl0bGUiOiJBbGxlbiBNb3VzZSBDQ0YiLCJ0eXBlcyI6WyJBdGxhc1NwYXRpYWxSZWZlcmVuY2VTeXN0ZW0iLCJCcmFpbkF0bGFzU3BhdGlhbFJlZmVyZW5jZVN5c3RlbSJdLCJyZXNvdXJjZSI6WyJiYnAiLCJhdGxhcyIsImh0dHBzOi8vYmJwLmVwZmwuY2gvbmV1cm9zY2llbmNlZ3JhcGgvZGF0YS9hbGxlbl9jY2Z2M19zcGF0aWFsX3JlZmVyZW5jZV9zeXN0ZW0iLDldfSx7ImlzRG93bmxvYWRhYmxlIjpmYWxzZSwiX3NlbGYiOiJodHRwczovL2JicC5lcGZsLmNoL25leHVzL3YxL3Jlc291cmNlcy9iYnAvYXRsYXMvZGF0YXNoYXBlczpvcmdhbml6YXRpb24vaHR0cHM6JTJGJTJGd3d3LmdyaWQuYWMlMkZpbnN0aXR1dGVzJTJGZ3JpZC40MTc4ODEuMyIsInRpdGxlIjoiQWxsZW4gSW5zdGl0dXRlIGZvciBCcmFpbiBTY2llbmNlIiwidHlwZXMiOlsiQWdlbnQiLCJPcmdhbml6YXRpb24iXSwicmVzb3VyY2UiOlsiYmJwIiwiYXRsYXMiLCJodHRwczovL3d3dy5ncmlkLmFjL2luc3RpdHV0ZXMvZ3JpZC40MTc4ODEuMyIsMV19XSwic2hyaW5rZWQiOmZhbHNlfSwicmVmZXJlciI6eyJwYXRobmFtZSI6Ii9iYnAvbG5tY2UvcmVzb3VyY2VzL2h0dHBzJTNBJTJGJTJGYmJwLmVwZmwuY2glMkZuZXVyb3NjaWVuY2VncmFwaCUyRmRhdGElMkZ0cmFjZXMlMkY2MDkwYjVmZS01NGJmLTQ0YjQtYWY1OS1hNDZhOWJiODFmYWMiLCJzZWFyY2giOiIiLCJzdGF0ZSI6eyJiYWNrZ3JvdW5kIjp7InBhdGhuYW1lIjoiL3NlYXJjaCIsInNlYXJjaCI6Ij9sYXlvdXQ9TmV1cm9uIEVsZWN0cm9waHlzaW9sb2d5IiwiaGFzaCI6IiIsImtleSI6ImdvNnNhZCJ9fX19'; const digestCurrentSelf = - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:volumetricdatalayer/cce06e96-85a4-403d-8b87-634a1554cdb9'; -const initialDataExplorerState = { + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:volumetricdatalayer/dca40f99-b494-4d2c-9a2f-c407180138b7'; +const initialDataExplorerState: TDataExplorerState = { current: { isDownloadable: false, _self: @@ -40,34 +39,40 @@ const initialDataExplorerState = { 1, ], }, - links: [ - { - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', - title: '001_141216_A1_CA1py_R_MPG', - types: ['Entity', 'Trace', 'Dataset'], - resource: [ - 'bbp', - 'lnmce', - 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', - ], - }, - { - isDownloadable: false, - _self: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', - title: 'Ecole Polytechnique Federale de Lausanne', - types: ['Organization', 'prov#Agent'], - resource: [ - 'bbp', - 'atlas', - 'https://www.grid.ac/institutes/grid.5333.6', - 1, - ], - }, - ], - shrinked: false, - highlightIndex: -1, + leftNodes: { + links: [ + { + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + title: '001_141216_A1_CA1py_R_MPG', + types: ['Entity', 'Trace', 'Dataset'], + resource: [ + 'bbp', + 'lnmce', + 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + 1, + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', + title: 'Ecole Polytechnique Federale de Lausanne', + types: ['Organization', 'prov#Agent'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.5333.6', + 1, + ], + }, + ], + shrinked: false, + }, + rightNodes: { + links: [], + shrinked: false, + }, limited: false, }; const fourthItemInStack = { @@ -106,9 +111,8 @@ describe('NavigationStack', () => { <> - - - + + @@ -130,18 +134,6 @@ describe('NavigationStack', () => { ); expect(navigationItems.length).toBe(2); }); - it('should render the correct number of NavigationStackItem components after hit the return back btn', async () => { - store.dispatch( - ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) - ); - const navigationBackItem = container.querySelector('.navigation-back-btn'); - navigationBackItem && (await user.click(navigationBackItem)); - rerender(app); - const navigationItemsAfterBack = container.querySelectorAll( - '.navigation-stack-item:not(.no-more)' - ); - expect(navigationItemsAfterBack.length).toBe(1); - }); it('should render the correct number of NavigationStackItem after multiple navigation', () => { store.dispatch( ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) @@ -174,11 +166,11 @@ describe('NavigationStack', () => { store.dispatch(AddNewNodeDataExplorerGraphFlow(fourthItemInStack)); rerender(app); const navigationItemsAfterMultipleNavigation = container.querySelectorAll( - '.navigation-stack-item:not(.no-more)' + '.navigation-stack-item:not(.more)' ); expect(navigationItemsAfterMultipleNavigation.length).toBe(5); const state = store.getState(); - expect(state.dataExplorer.links.length).toBe(5); + expect(state.dataExplorer.leftNodes.links.length).toBe(5); }); it('should render the NavigationStackShrinkedItem when it passed MAX_NAVIGATION_ITEMS_IN_STACK', () => { store.dispatch( @@ -255,7 +247,6 @@ describe('NavigationStack', () => { store.dispatch( ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) ); - rerender(app); store.dispatch( AddNewNodeDataExplorerGraphFlow({ isDownloadable: false, @@ -320,10 +311,10 @@ describe('NavigationStack', () => { expect(openShrinkedNavList).toBeInTheDocument(); openShrinkedNavList && (await user.click(openShrinkedNavList)); rerender(app); - const collapseBtn = container.querySelector('.navigation-collapse-btn'); + const collapseBtn = container.querySelector('.collapse-btn'); expect(collapseBtn).not.toBeNull(); expect(collapseBtn).toBeInTheDocument(); - expect(store.getState().dataExplorer.shrinked).toBe(false); + expect(store.getState().dataExplorer.leftNodes.shrinked).toBe(false); }); it('should the items in the stack be 4 when the user jump to the 5th item', async () => { store.dispatch( @@ -386,17 +377,22 @@ describe('NavigationStack', () => { }) ); rerender(app); - const forthNode = container.querySelector( - '.navigation-stack-item.item-4 > .anticon.anticon-plus' + // 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"]' ); - expect(forthNode).not.toBeNull(); - expect(forthNode).toBeInTheDocument(); - forthNode && (await user.click(forthNode)); + expect(forthNodeNavigationItem).not.toBeNull(); + expect(forthNodeNavigationItem).toBeInTheDocument(); + forthNodeNavigationItem && (await user.click(forthNodeNavigationItem)); rerender(app); - const navigationItems = container.querySelectorAll( - '.navigation-stack-item:not(.no-more)' + const navigationLeftItems = container.querySelectorAll( + '.navigation-stack-item.left:not(.more)' + ); + expect(navigationLeftItems.length).toBe(4); + const navigationRightItems = container.querySelectorAll( + '.navigation-stack-item.right:not(.more)' ); - expect(navigationItems.length).toBe(4); + expect(navigationRightItems.length).toBe(3); const state = store.getState(); expect(state.dataExplorer.current._self).toEqual(fourthItemInStack._self); }); @@ -405,8 +401,8 @@ describe('NavigationStack', () => { store.dispatch(PopulateDataExplorerGraphFlow(sampleDigest)); rerender(app); const dataExplorerState = store.getState().dataExplorer; - expect(dataExplorerState.links.length).toBe(2); + expect(dataExplorerState.leftNodes.links.length).toBe(2); + expect(dataExplorerState.rightNodes.links.length).toBe(2); expect(dataExplorerState.current._self).toBe(digestCurrentSelf); - expect(dataExplorerState.shrinked).toBe(false); }); }); From 44df550cfe0231d7c6aa274b35c99a43874a7056 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:27:49 +0200 Subject: [PATCH 125/192] d-4044/update: update de-gf tests --- .../DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx index 8f48c74d5..9d812189b 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx @@ -124,12 +124,13 @@ describe('DataExplorerGraphFlow', () => { ); history.push('/another-page'); const dataExplorerState = store.getState().dataExplorer; - const localstorage = localStorage.getItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + const localstorage = sessionStorage.getItem( + DATA_EXPLORER_GRAPH_FLOW_DIGEST + ); expect(localstorage).toBeNull(); - expect(dataExplorerState.links.length).toBe(0); + expect(dataExplorerState.leftNodes.links.length).toBe(0); + expect(dataExplorerState.rightNodes.links.length).toBe(0); expect(dataExplorerState.current).toBeNull(); - expect(dataExplorerState.shrinked).toBe(false); expect(dataExplorerState.limited).toBe(false); - expect(dataExplorerState.highlightIndex).toBe(-1); }); }); From 965cf02c95c545590a82c15945ca2e2003bc097a Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Wed, 12 Jul 2023 14:34:32 +0200 Subject: [PATCH 126/192] d-4044/update: update editor bg --- src/shared/components/ResourceEditor/ResourceEditor.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index a7faf3755..35fc6bc3b 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -29,7 +29,7 @@ } .cm-s-base16-light.CodeMirror { - background-color: white; + background-color: #f5f5f5 !important; } .resource-editor { From cda6d1696bf46d4b5a9d15115d8609d9f44b977b Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:17:37 +0200 Subject: [PATCH 127/192] f-4046/update: the DE back/forward state actions --- src/shared/store/reducers/data-explorer.ts | 63 +++++++++++++++++----- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 8856d169f..3fd0907b1 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -1,5 +1,14 @@ import { createSlice } from '@reduxjs/toolkit'; -import { slice, clone, dropRight, nth, last, concat } from 'lodash'; +import { + slice, + clone, + dropRight, + nth, + last, + concat, + first, + drop, +} from 'lodash'; type TProject = string; type TOrganization = string; @@ -223,22 +232,51 @@ export const dataExplorerSlice = createSlice({ return newState; }, ReturnBackDataExplorerGraphFlow: state => { - const current = last(state.leftNodes.links) as TDELink; - const newrightNodesLinks = state.rightNodes.links; - const newleftNodesLinks = dropRight(state.leftNodes.links) as TDELink[]; - insert(newrightNodesLinks, 0, state.current); + const newCurrent = last(state.leftNodes.links) as TDELink; + const current = state.current; + const newRightNodesLinks = concat( + current ? [current] : [], + state.rightNodes.links + ); + const newLeftNodesLinks = dropRight(state.leftNodes.links) as TDELink[]; const rightNodes = { - links: newrightNodesLinks, - shrinked: isShrinkable(newrightNodesLinks), + links: newRightNodesLinks, + shrinked: isShrinkable(newRightNodesLinks), + }; + const leftNodes = { + links: newLeftNodesLinks, + shrinked: isShrinkable(newLeftNodesLinks), }; const newState = { ...state, - current, rightNodes, - leftNodes: { - links: newleftNodesLinks, - shrinked: isShrinkable(newleftNodesLinks), - }, + leftNodes, + current: newCurrent, + }; + calculateNewDigest(newState); + return newState; + }, + MoveForwardDataExplorerGraphFlow: state => { + const newCurrent = first(state.rightNodes.links) as TDELink; + const current = state.current; + const newLeftNodesLinks = concat( + state.leftNodes.links, + current ? [current] : [] + ); + const newRightNodesLinks = drop(state.rightNodes.links) as TDELink[]; + const rightNodes = { + links: newRightNodesLinks, + shrinked: isShrinkable(newRightNodesLinks), + }; + const leftNodes = { + links: newLeftNodesLinks, + shrinked: isShrinkable(newLeftNodesLinks), + }; + const newState = { + ...state, + rightNodes, + leftNodes, + current: newCurrent, }; calculateNewDigest(newState); return newState; @@ -310,6 +348,7 @@ export const { ShrinkNavigationStackDataExplorerGraphFlow, JumpToNodeDataExplorerGraphFlow, ReturnBackDataExplorerGraphFlow, + MoveForwardDataExplorerGraphFlow, ResetDataExplorerGraphFlow, InitDataExplorerGraphFlowLimitedVersion, } = dataExplorerSlice.actions; From b823f15283133aefc2500f863990d25d86d198f0 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:18:09 +0200 Subject: [PATCH 128/192] f-4046/update: add navigation back/forward fns --- .../useNavigationStack.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts index d37d1e06b..5046c5515 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts @@ -1,14 +1,19 @@ import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router'; +import { useHistory } from 'react-router'; import { RootState } from '../../store/reducers'; import { + DATA_EXPLORER_GRAPH_FLOW_DIGEST, ExpandNavigationStackDataExplorerGraphFlow, + MoveForwardDataExplorerGraphFlow, + ResetDataExplorerGraphFlow, + ReturnBackDataExplorerGraphFlow, ShrinkNavigationStackDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; const useNavigationStackManager = () => { const dispatch = useDispatch(); - const { rightNodes, leftNodes } = useSelector( + const history = useHistory(); + const { rightNodes, leftNodes, referer } = useSelector( (state: RootState) => state.dataExplorer ); const leftShrinked = leftNodes.shrinked; @@ -25,6 +30,27 @@ const useNavigationStackManager = () => { const onRightExpand = () => dispatch(ExpandNavigationStackDataExplorerGraphFlow({ side: 'right' })); + const backArrowVisible = leftLinks.length > 0 || !!referer?.pathname; + const forwardArrowVisible = rightLinks.length > 0; + + const onNavigateBack = () => { + if (referer?.pathname && !leftLinks.length) { + dispatch(ResetDataExplorerGraphFlow({ initialState: null })); + localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); + history.push(`${referer.pathname}${referer.search}`, { + ...referer.state, + }); + return; + } + history.replace(location.pathname); + dispatch(ReturnBackDataExplorerGraphFlow()); + }; + + const onNavigateForward = () => { + history.replace(location.pathname); + dispatch(MoveForwardDataExplorerGraphFlow()); + }; + return { leftShrinked, rightShrinked, @@ -34,6 +60,10 @@ const useNavigationStackManager = () => { onLeftExpand, onRightShrink, onRightExpand, + onNavigateBack, + onNavigateForward, + backArrowVisible, + forwardArrowVisible, }; }; From 1aa5a5c17f81502fd417e22b0d9e2cf28fd25426 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:18:42 +0200 Subject: [PATCH 129/192] f-4046/clean: remove previous back btn --- .../NavigationBackButton.tsx | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx deleted file mode 100644 index 63160803c..000000000 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { useHistory, useLocation } from 'react-router'; -import { ArrowLeftOutlined } from '@ant-design/icons'; -import { clsx } from 'clsx'; -import { - DATA_EXPLORER_GRAPH_FLOW_DIGEST, - ResetDataExplorerGraphFlow, - ReturnBackDataExplorerGraphFlow, -} from '../../store/reducers/data-explorer'; -import { RootState } from '../../store/reducers'; -import './styles.less'; - -const NavigationBack = () => { - const dispatch = useDispatch(); - const history = useHistory(); - const location = useLocation(); - const { referer } = useSelector((state: RootState) => state.dataExplorer); - - // const onBack = () => { - // if (referer?.pathname && !links.length) { - // dispatch(ResetDataExplorerGraphFlow({ initialState: null })); - // localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); - // history.push(`${referer.pathname}${referer.search}`, { - // ...referer.state, - // }); - // return; - // } - // history.replace(location.pathname); - // dispatch(ReturnBackDataExplorerGraphFlow()); - // }; - - // return links.length || !!referer?.pathname ? ( - // - // ) : null; - return null; -}; - -export default NavigationBack; From fa80298b7602b1f70c88287d9e9e5427709f9a2d Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:19:02 +0200 Subject: [PATCH 130/192] f-4046/clean: remove unnecessary code --- src/shared/components/ResourceEditor/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index ef38c4660..1c22bd1d7 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -91,7 +91,6 @@ const ResourceEditor: React.FunctionComponent = props => { const { dataExplorer: { limited }, oidc, - config: { apiEndpoint }, } = useSelector((state: RootState) => ({ dataExplorer: state.dataExplorer, oidc: state.oidc, From 8a2f7fd1d167afcf8320570ab7287eae68f8f471 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:19:48 +0200 Subject: [PATCH 131/192] f-4046/ref: update exports to index.ts --- src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts | 2 +- .../organisms/DataExplorerGraphFlowNavigationStack/index.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/index.ts diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index edc413476..e1fc1cd80 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1,5 +1,5 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; -export { default as NavigationBackButton } from './NavigationBackButton'; +export { default as NavigationArrow } from './NavigationArrow'; export { default as NavigationStackItem } from './NavigationStackItem'; export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; export { default as NavigationCollapseButton } from './NavigationCollapseButton'; diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/index.ts b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/index.ts new file mode 100644 index 000000000..c5cfa73f3 --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/index.ts @@ -0,0 +1,2 @@ +export { default as NavigationArrows } from './NavigationArrows'; +export { default as NavigationStack } from './NavigationStack'; From 5113687e1f750e4593665a054539602859f6876b Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:20:31 +0200 Subject: [PATCH 132/192] f-4046/update: add navigation arrows --- .../NavigationArrow.tsx | 31 ++++++++++++++++ .../styles.less | 36 +++++++++---------- .../NavigationArrows.tsx | 34 ++++++++++++++++++ 3 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationArrow.tsx create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.tsx diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationArrow.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationArrow.tsx new file mode 100644 index 000000000..e53cca4d8 --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationArrow.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons'; +import './styles.less'; + +type NavigationArrowDirection = 'back' | 'forward'; + +const NavigationArrow = ({ + direction, + visible, + title, + onClick, +}: { + direction: NavigationArrowDirection; + visible: boolean; + title: string; + onClick: () => void; +}) => { + return visible ? ( + + ) : null; +}; + +export default NavigationArrow; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index a08136a77..84c10c249 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -154,37 +154,37 @@ } } -.navigation-back-btn { - margin-top: 8px; - background: white; - box-shadow: 0 2px 12px rgba(#333, 0.12); - padding: 5px 15px; +.navigation-arrow-btn { + cursor: pointer; + background: transparent; max-width: max-content; max-height: 30px; margin-top: 10px; - border-radius: 4px; - border: 1px solid #afacacd8; - cursor: pointer; display: flex; align-items: center; justify-content: center; 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; } - - &:hover { - span { - color: @fusion-daybreak-7; - } - } - - &.go-back-to-referer { - margin-left: 40px; + &:disabled { + cursor: not-allowed; + opacity: 0.5; + color: #afacacd8; } } diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.tsx new file mode 100644 index 000000000..42febe91c --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { NavigationArrow } from '../../molecules/DataExplorerGraphFlowMolecules'; +import useNavigationStackManager from './useNavigationStack'; +import './styles.less'; + +const NavigationArrows = () => { + const { + onNavigateBack, + onNavigateForward, + backArrowVisible, + forwardArrowVisible, + } = useNavigationStackManager(); + + return ( +
+ + +
+ ); +}; + +export default NavigationArrows; From 2c74f54726ffa4a4daf5ace4234c1f7d5e94aa35 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:21:18 +0200 Subject: [PATCH 133/192] f-4046/update: integrate navigation arrows to the degf page --- .../DateExplorerGraphFlow.tsx | 38 ++++++------------- .../canvas/DataExplorerGraphFlow/styles.less | 2 +- .../styles.less | 9 +++++ 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index 5413a260b..20df74f14 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -9,10 +9,14 @@ import { PopulateDataExplorerGraphFlow, ResetDataExplorerGraphFlow, } from '../../store/reducers/data-explorer'; -import NavigationStack from '../../organisms/DataExplorerGraphFlowNavigationStack/NavigationStack'; +import { + NavigationArrows, + NavigationStack, +} from '../../organisms/DataExplorerGraphFlowNavigationStack'; import DataExplorerContentPage from '../../organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent'; -import ResourceResolutionCache from '../../components/ResourceEditor/ResourcesLRUCache'; import useNavigationStackManager from '../../organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack'; +import ResourceResolutionCache from '../../components/ResourceEditor/ResourcesLRUCache'; +import DataExplorerGraphFlowEmpty from './DataExplorerGraphFlowEmpty'; import './styles.less'; @@ -30,10 +34,6 @@ const DataExplorerGraphFlow = () => { rightShrinked, leftLinks, rightLinks, - onLeftShrink, - onLeftExpand, - onRightShrink, - onRightExpand, } = useNavigationStackManager(); useEffect(() => { @@ -61,32 +61,15 @@ const DataExplorerGraphFlow = () => { ResourceResolutionCache.clear(); }; }, [ResourceResolutionCache]); - if (current === null) { - return ( -
-
- nodes -
No data explorer graph flow
-
- Please select a node from any resource view editor to start - exploring -
-
-
- ); - } - return ( + + return !current ? ( + + ) : (
{
)}
+
{!!rightLinks.length && ( diff --git a/src/shared/canvas/DataExplorerGraphFlow/styles.less b/src/shared/canvas/DataExplorerGraphFlow/styles.less index e0942bc71..91dcb0816 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/styles.less +++ b/src/shared/canvas/DataExplorerGraphFlow/styles.less @@ -9,7 +9,7 @@ justify-content: flex-start; background-color: @fusion-main-bg; gap: 10px; - + margin-top: 52px; &.no-links { grid-template-columns: 1fr; } diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less index 5575a9b71..d512000bd 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/styles.less @@ -21,3 +21,12 @@ height: 100%; min-height: 100vh; } + +.navigation-arrows { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 10px 20px; +} From e5fd3751c276d3040fd006985125a47b27f5dea8 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:21:40 +0200 Subject: [PATCH 134/192] f-4046/ref: empty page when no degf current item --- .../DataExplorerGraphFlowEmpty.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/shared/canvas/DataExplorerGraphFlow/DataExplorerGraphFlowEmpty.tsx diff --git a/src/shared/canvas/DataExplorerGraphFlow/DataExplorerGraphFlowEmpty.tsx b/src/shared/canvas/DataExplorerGraphFlow/DataExplorerGraphFlowEmpty.tsx new file mode 100644 index 000000000..56c30e4e0 --- /dev/null +++ b/src/shared/canvas/DataExplorerGraphFlow/DataExplorerGraphFlowEmpty.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const DataExplorerGraphFlowEmpty = () => { + return ( +
+
+ nodes +
No data explorer graph flow
+
+ Please select a node from any resource view editor to start exploring +
+
+
+ ); +}; + +export default DataExplorerGraphFlowEmpty; From b93a702d30d07616f3515da86ec1822cc2e52550 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:22:02 +0200 Subject: [PATCH 135/192] f-4046/update: add tests for navigation arrows btns --- .../NavigationArrows.spec.tsx | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.spec.tsx diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.spec.tsx new file mode 100644 index 000000000..40fed18b2 --- /dev/null +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.spec.tsx @@ -0,0 +1,238 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, RenderResult } from '@testing-library/react'; +import { AnyAction, Store } from 'redux'; +import { Provider } from 'react-redux'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import { createNexusClient, NexusClient } from '@bbp/nexus-sdk'; +import { Router } from 'react-router-dom'; +import userEvent from '@testing-library/user-event'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +import { NexusProvider } from '@bbp/react-nexus'; +import { deltaPath } from '../../../__mocks__/handlers/handlers'; +import configureStore from '../../store'; +import { + AddNewNodeDataExplorerGraphFlow, + ResetDataExplorerGraphFlow, + PopulateDataExplorerGraphFlow, + TDataExplorerState, +} from '../../store/reducers/data-explorer'; +import NavigationArrows from './NavigationArrows'; +import NavigationStack from './NavigationStack'; + +const initialDataExplorerState: TDataExplorerState = { + current: { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.417881.3', + title: 'Allen Institute for Brain Science', + types: ['Agent', 'Organization'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.417881.3', + 1, + ], + }, + leftNodes: { + links: [ + { + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/lnmce/datashapes:dataset/traces%2F460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + title: '001_141216_A1_CA1py_R_MPG', + types: ['Entity', 'Trace', 'Dataset'], + resource: [ + 'bbp', + 'lnmce', + 'https://bbp.epfl.ch/neurosciencegraph/data/traces/460bfa2e-cb7d-4420-a448-2030a6bf4ae4', + 1, + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/bbp/atlas/datashapes:organization/https:%2F%2Fwww.grid.ac%2Finstitutes%2Fgrid.5333.6', + title: 'Ecole Polytechnique Federale de Lausanne', + types: ['Organization', 'prov#Agent'], + resource: [ + 'bbp', + 'atlas', + 'https://www.grid.ac/institutes/grid.5333.6', + 1, + ], + }, + ], + shrinked: false, + }, + rightNodes: { + links: [ + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/_/https:%2F%2Fneuroshapes.org', + title: 'neuroshapes.org', + types: [], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://neuroshapes.org', + 161, + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/datashapes:ontologyentity/https:%2F%2Fbbp.epfl.ch%2Fontologies%2Fcore%2Fefeatures%2FNeuroElectroNeuronElectrophysiologicalFeature', + title: 'NeuroElectro Neuron Electrophysiological Feature', + types: ['Class'], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://bbp.epfl.ch/ontologies/core/efeatures/NeuroElectroNeuronElectrophysiologicalFeature', + 29, + ], + }, + { + isDownloadable: false, + _self: + 'https://bbp.epfl.ch/nexus/v1/resources/neurosciencegraph/datamodels/_/https:%2F%2Fneuroshapes.org', + title: 'neuroshapes.org', + types: [], + resource: [ + 'neurosciencegraph', + 'datamodels', + 'https://neuroshapes.org', + 161, + ], + }, + ], + shrinked: false, + }, + limited: false, + referer: { + pathname: '/my-data', + search: '', + state: {}, + }, +}; + +describe('NavigationStack', () => { + let app: JSX.Element; + let component: RenderResult; + let container: HTMLElement; + let rerender: (ui: React.ReactElement) => void; + let store: Store; + let user: UserEvent; + let history: MemoryHistory<{}>; + let nexus: NexusClient; + + beforeEach(() => { + history = createMemoryHistory({}); + + nexus = createNexusClient({ + fetch, + uri: deltaPath(), + }); + store = configureStore(history, { nexus }, {}); + app = ( + + + + <> + + + + + + + + ); + component = render(app); + container = component.container; + rerender = component.rerender; + user = userEvent.setup(); + }); + + it('should render the back/forward arrows as not disabled', () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); + rerender(app); + + const backArrow = container.querySelector('[aria-label="back-arrow"]'); + const forwardArrow = container.querySelector( + '[aria-label="forward-arrow"]' + ); + expect(backArrow).toBeInTheDocument(); + expect(forwardArrow).toBeInTheDocument(); + }); + it('should left side of navigation become 3 and right side become 2 when Forward btn clicked', async () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); + rerender(app); + const forwardArrow = container.querySelector( + '.navigation-arrow-btn[aria-label="forward-arrow"]' + ); + expect(forwardArrow).not.toBeNull(); + forwardArrow && (await user.click(forwardArrow)); + rerender(app); + expect(store.getState().dataExplorer.leftNodes.links.length).toEqual(3); + expect(store.getState().dataExplorer.rightNodes.links.length).toEqual(2); + }); + it('should left side of navigation become 1 and right side become 4 when Back btn clicked', async () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); + rerender(app); + const backArrow = container.querySelector( + '.navigation-arrow-btn[aria-label="back-arrow"]' + ); + expect(backArrow).not.toBeNull(); + backArrow && (await user.click(backArrow)); + rerender(app); + expect(store.getState().dataExplorer.leftNodes.links.length).toEqual(1); + expect(store.getState().dataExplorer.rightNodes.links.length).toEqual(4); + }); + it('should forward btn disappear when there is no more forward navigation', async () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); + rerender(app); + for (const _ of store.getState().dataExplorer.rightNodes.links) { + const forwardArrow = container.querySelector( + '.navigation-arrow-btn[aria-label="forward-arrow"]' + ); + expect(forwardArrow).not.toBeNull(); + forwardArrow && (await user.click(forwardArrow)); + rerender(app); + } + expect(store.getState().dataExplorer.leftNodes.links.length).toEqual(5); + expect(store.getState().dataExplorer.rightNodes.links.length).toEqual(0); + const forwardArrowAfterFullNavigation = container.querySelector( + '.navigation-arrow-btn[aria-label="forward-arrow"]' + ); + expect(forwardArrowAfterFullNavigation).toBeNull(); + }); + it('should return to /my-data when there is no more back navigation', async () => { + store.dispatch( + ResetDataExplorerGraphFlow({ initialState: initialDataExplorerState }) + ); + rerender(app); + for (const _ of store.getState().dataExplorer.leftNodes.links) { + const backArrow = container.querySelector( + '.navigation-arrow-btn[aria-label="back-arrow"]' + ); + expect(backArrow).not.toBeNull(); + backArrow && (await user.click(backArrow)); + rerender(app); + } + expect(store.getState().dataExplorer.leftNodes.links.length).toEqual(0); + const lastBackArrow = container.querySelector( + '.navigation-arrow-btn[aria-label="back-arrow"]' + ); + expect(lastBackArrow).not.toBeNull(); + lastBackArrow && (await user.click(lastBackArrow)); + expect(history.location.pathname).toEqual('/my-data'); + }); +}); From af18b5e0a69a27ed3b68fc1091b5a811807a4468 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 10:32:13 +0200 Subject: [PATCH 136/192] f-4044/clean: code style --- .../DateExplorerGraphFlow.spec.tsx | 4 +- .../ResourceEditor/ResourceEditor.less | 2 +- .../NavigationBackButton.tsx | 48 ------------------- .../DataExplorerGraphFlowMolecules/index.ts | 1 - 4 files changed, 3 insertions(+), 52 deletions(-) delete mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx index 9d812189b..23191c941 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx @@ -124,10 +124,10 @@ describe('DataExplorerGraphFlow', () => { ); history.push('/another-page'); const dataExplorerState = store.getState().dataExplorer; - const localstorage = sessionStorage.getItem( + const sessionStorageItem = sessionStorage.getItem( DATA_EXPLORER_GRAPH_FLOW_DIGEST ); - expect(localstorage).toBeNull(); + expect(sessionStorageItem).toBeNull(); expect(dataExplorerState.leftNodes.links.length).toBe(0); expect(dataExplorerState.rightNodes.links.length).toBe(0); expect(dataExplorerState.current).toBeNull(); diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 35fc6bc3b..a4da533ae 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -29,7 +29,7 @@ } .cm-s-base16-light.CodeMirror { - background-color: #f5f5f5 !important; + background-color: @fusion-main-bg !important; } .resource-editor { diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx deleted file mode 100644 index 63160803c..000000000 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/NavigationBackButton.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { useHistory, useLocation } from 'react-router'; -import { ArrowLeftOutlined } from '@ant-design/icons'; -import { clsx } from 'clsx'; -import { - DATA_EXPLORER_GRAPH_FLOW_DIGEST, - ResetDataExplorerGraphFlow, - ReturnBackDataExplorerGraphFlow, -} from '../../store/reducers/data-explorer'; -import { RootState } from '../../store/reducers'; -import './styles.less'; - -const NavigationBack = () => { - const dispatch = useDispatch(); - const history = useHistory(); - const location = useLocation(); - const { referer } = useSelector((state: RootState) => state.dataExplorer); - - // const onBack = () => { - // if (referer?.pathname && !links.length) { - // dispatch(ResetDataExplorerGraphFlow({ initialState: null })); - // localStorage.removeItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST); - // history.push(`${referer.pathname}${referer.search}`, { - // ...referer.state, - // }); - // return; - // } - // history.replace(location.pathname); - // dispatch(ReturnBackDataExplorerGraphFlow()); - // }; - - // return links.length || !!referer?.pathname ? ( - // - // ) : null; - return null; -}; - -export default NavigationBack; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index edc413476..e538b978e 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1,5 +1,4 @@ export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; -export { default as NavigationBackButton } from './NavigationBackButton'; export { default as NavigationStackItem } from './NavigationStackItem'; export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; export { default as NavigationCollapseButton } from './NavigationCollapseButton'; From 8a7bcd1e6f1ad699fdde689972834e17eb29b507 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 13:01:51 +0200 Subject: [PATCH 137/192] f-4047/update: rename limited to fullscreen mode f-4047/update: update state action for fullscreen mode --- src/server/index.tsx | 2 +- src/shared/store/reducers/data-explorer.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/server/index.tsx b/src/server/index.tsx index 379d42283..b0ff06aae 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -150,7 +150,7 @@ app.get('*', async (req: express.Request, res: express.Response) => { current: null, leftNodes: { links: [], shrinked: false }, rightNodes: { links: [], shrinked: false }, - limited: false, + fullscreen: false, }, }; diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 3fd0907b1..883d8eeb8 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -54,7 +54,7 @@ export type TDataExplorerState = { search: string; state: Record; } | null; - limited: boolean; + fullscreen: boolean; }; export type TNavigationStackSide = 'left' | 'right'; @@ -67,7 +67,7 @@ const initialState: TDataExplorerState = { rightNodes: { links: [], shrinked: false }, current: null, referer: null, - limited: false, + fullscreen: false, }; const calculateNewDigest = (state: TDataExplorerState) => { @@ -108,13 +108,13 @@ export const dataExplorerSlice = createSlice({ }, InitNewVisitDataExplorerGraphView: ( state, - { payload: { source, current, limited, referer } } + { payload: { source, current, fullscreen, referer } } ) => { const newState = { ...state, referer, current, - limited, + fullscreen, leftNodes: { links: source && current @@ -330,10 +330,13 @@ export const dataExplorerSlice = createSlice({ ResetDataExplorerGraphFlow: (_, action) => { return action.payload.initialState ?? initialState; }, - InitDataExplorerGraphFlowLimitedVersion: (state, action) => { + InitDataExplorerGraphFlowFullscreenVersion: ( + state, + { payload: { fullscreen } }: { payload: { fullscreen?: boolean } } + ) => { const newState = { ...state, - limited: action.payload ?? !state.limited, + fullscreen: fullscreen ?? !state.fullscreen, }; calculateNewDigest(newState); return newState; @@ -350,7 +353,7 @@ export const { ReturnBackDataExplorerGraphFlow, MoveForwardDataExplorerGraphFlow, ResetDataExplorerGraphFlow, - InitDataExplorerGraphFlowLimitedVersion, + InitDataExplorerGraphFlowFullscreenVersion, } = dataExplorerSlice.actions; export default dataExplorerSlice.reducer; From ac86d53d07449b759b0e197b46901dd9e180a297 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 13:03:01 +0200 Subject: [PATCH 138/192] f-4047/update: fullscreen header and the toggle styles --- src/shared/App.less | 64 ++++++++++++++++--- .../ContentFullscreenHeader.tsx | 34 ++++++++++ .../ContentLimitedHeader.tsx | 29 --------- .../styles.less | 64 ++----------------- 4 files changed, 92 insertions(+), 99 deletions(-) create mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/ContentFullscreenHeader.tsx delete mode 100644 src/shared/molecules/DataExplorerGraphFlowMolecules/ContentLimitedHeader.tsx diff --git a/src/shared/App.less b/src/shared/App.less index 7d47123bc..1637f51b5 100644 --- a/src/shared/App.less +++ b/src/shared/App.less @@ -9,27 +9,39 @@ margin: 52px auto 0; display: flex; background: #f5f5f5 !important; + &.-unconstrained-width { padding: 2em; max-width: none; } + &.resource-view { - max-width: 60%; + max-width: 1320px; background-color: @primary-card; - background-image: linear-gradient( - 315deg, - @primary-card 0%, - @subtle-white 74% - ); min-height: calc(100vh - 40px); transition: background-image ease-out 1s; + + &.background { + max-width: 60%; + background-image: linear-gradient( + 315deg, + @primary-card 0%, + @subtle-white 74% + ); + + .resource-details { + .highShadow(); + padding: 1em; + width: 100%; + background-color: @background-color-subtle; + } + } + .resource-details { - .highShadow(); - padding: 1em; - width: 100%; - background-color: @background-color-subtle; + background-color: @fusion-main-bg; } } + &.data-explorer-container { width: fit-content; min-width: calc(100vw - 1rem); @@ -55,6 +67,7 @@ .ant-alert-warning { margin: 1em 0; } + section.links { width: 48%; } @@ -64,6 +77,7 @@ .identities-list { margin: 0; padding: 0; + .list-item { cursor: auto; } @@ -76,6 +90,7 @@ .ant-pagination-item { margin-right: 2px; } + .ant-list-pagination { text-align: center; } @@ -83,6 +98,7 @@ .ant-input-affix-wrapper .ant-input-suffix { color: rgba(0, 0, 0, 0.2); } + .ant-upload.ant-upload-drag .ant-upload { padding: @default-pad; } @@ -97,6 +113,7 @@ .studio-view { padding: 0 2em; + .workspace { display: flex; width: 100%; @@ -104,6 +121,7 @@ min-width: 800px; min-height: 600px; } + .studio-back-button { margin-bottom: 5px; } @@ -211,3 +229,29 @@ outline: none; border-radius: 0; } + +.full-screen-switch__wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + span { + color: #003a8c; + } + .full-screen-switch { + border-color: #2e76bf !important; + background: linear-gradient( + 0deg, + rgba(0, 58, 140, 0.3), + rgba(0, 58, 140, 0.3) + ), + 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: #002766; + } + } + } +} diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentFullscreenHeader.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentFullscreenHeader.tsx new file mode 100644 index 000000000..2f259bfff --- /dev/null +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentFullscreenHeader.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Switch } from 'antd'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from 'shared/store/reducers'; +import { InitDataExplorerGraphFlowFullscreenVersion } from '../../store/reducers/data-explorer'; +import './styles.less'; + +const DataExplorerGraphFlowContentLimitedHeader = () => { + const dispatch = useDispatch(); + const { current, fullscreen } = useSelector( + (state: RootState) => state.dataExplorer + ); + const onStandardScreen = () => + dispatch(InitDataExplorerGraphFlowFullscreenVersion({ fullscreen: false })); + + return ( +
+
+ Fullscreen + +
+

+ {current?.title} +

+
+ ); +}; + +export default DataExplorerGraphFlowContentLimitedHeader; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentLimitedHeader.tsx b/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentLimitedHeader.tsx deleted file mode 100644 index 41914dedf..000000000 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/ContentLimitedHeader.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useState } from 'react'; -import { Switch } from 'antd'; -import { useSelector, useDispatch } from 'react-redux'; -import { RootState } from 'shared/store/reducers'; -import { InitDataExplorerGraphFlowLimitedVersion } from '../../store/reducers/data-explorer'; -import './styles.less'; - -const DataExplorerGraphFlowContentLimitedHeader = () => { - const dispatch = useDispatch(); - const { current, limited } = useSelector( - (state: RootState) => state.dataExplorer - ); - const onSelectWrite = (checked: boolean) => { - dispatch(InitDataExplorerGraphFlowLimitedVersion(!checked)); - }; - - return ( -
-
{current?.title}
-
- Read - - Write -
-
- ); -}; - -export default DataExplorerGraphFlowContentLimitedHeader; diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less index 84c10c249..b3a2b5346 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/styles.less @@ -188,44 +188,20 @@ } } -.degf-content__haeder { +.degf-content__header { display: flex; align-items: center; - justify-content: space-between; gap: 20px; - margin: 20px 5px; + margin: 0 5px 10px; .title { + margin-left: 50px; + margin-bottom: 0; font-weight: 700; font-size: 16px; line-height: 140%; color: @fusion-menu-color; } - - .switcher { - .ant-switch { - border: 1px solid #bfbfbf; - background-color: white !important; - margin: 0 5px; - } - - .ant-switch-handle { - top: 1px; - } - - .ant-switch-handle::before { - background-color: @fusion-menu-color !important; - } - - span { - user-select: none; - font-weight: 700; - font-size: 12px; - line-height: 130%; - letter-spacing: 0.01em; - color: @fusion-menu-color; - } - } } .navigation-collapse-btn { @@ -239,35 +215,3 @@ border: 1px solid #afacacd8; cursor: pointer; } - -@keyframes vibrate { - 0% { - -webkit-transform: translate(0); - transform: translate(0); - } - - 20% { - -webkit-transform: translate(-1.5px, 1.5px); - transform: translate(-1.5px, 1.5px); - } - - 40% { - -webkit-transform: translate(-1.5px, -1.5px); - transform: translate(-1.5px, -1.5px); - } - - 60% { - -webkit-transform: translate(1.5px, 1.5px); - transform: translate(1.5px, 1.5px); - } - - 80% { - -webkit-transform: translate(1.5px, -1.5px); - transform: translate(1.5px, -1.5px); - } - - 100% { - -webkit-transform: translate(0); - transform: translate(0); - } -} From 1e92d9ca2382a87aa5d6a2553db46fc8a34940eb Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 13:04:38 +0200 Subject: [PATCH 139/192] f-4047/update: update fullscreen state use --- .../ResourceEditor/ResourceEditor.less | 22 ++++ .../components/ResourceEditor/index.tsx | 109 ++++++++++-------- src/shared/containers/ResourceEditor.tsx | 8 +- .../DataExplorerGraphFlowContent.tsx | 8 +- 4 files changed, 89 insertions(+), 58 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 35fc6bc3b..96d7625b6 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -69,14 +69,18 @@ &.wait-for-tooltip { cursor: progress !important; } + &.has-tooltip { cursor: pointer !important; } + &.error { cursor: not-allowed !important; } + &.downloadable { } + &::after { content: ''; display: inline-block; @@ -96,12 +100,14 @@ &.resolution-on-progress { .CodeMirror-lines { user-select: none; + .fusion-resource-link { cursor: progress !important; } } } } + .CodeMirror-hover-tooltip-popover, .CodeMirror-hover-tooltip { background-color: white; @@ -116,25 +122,31 @@ white-space: pre-wrap; transition: all 0.4s ease-in-out; padding: 4px 0; + &.popover { background-color: white !important; + .CodeMirror-hover-tooltip-resources-content { display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; + .CodeMirror-hover-tooltip-item { width: 100%; padding: 4px; align-items: center; justify-content: flex-start; cursor: pointer; + .tag { background-color: @fusion-primary-color; } + &:hover { background-color: @fusion-blue-0; color: @fusion-primary-color; + .tag { background-color: white; color: @fusion-primary-color; @@ -143,6 +155,7 @@ } } } + &-content { display: flex; flex-direction: column; @@ -183,6 +196,7 @@ padding: 2px 10px 2px 5px; overflow: hidden; user-select: none; + .tag { color: white; padding: 2px 5px; @@ -204,3 +218,11 @@ } } } + +.editor-controls-panel { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + width: 100%; +} diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 1c22bd1d7..cff247452 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -89,7 +89,7 @@ const ResourceEditor: React.FunctionComponent = props => { JSON.stringify(rawData, null, 2) ); const { - dataExplorer: { limited }, + dataExplorer: { fullscreen }, oidc, } = useSelector((state: RootState) => ({ dataExplorer: state.dataExplorer, @@ -220,58 +220,65 @@ const ResourceEditor: React.FunctionComponent = props => { )}
-
- {showFullScreen && ( +
+
+ {showFullScreen && ( +
+ Fullscreen + +
+ )} +
+
- )} - - {!expanded && !isEditing && valid && showMetadataToggle && ( - onMetadataChangeFold(checked)} - style={switchMarginRight} - /> - )} - {showExpanded && !isEditing && valid && ( - onFormatChangeFold(expanded)} - style={switchMarginRight} - /> - )} - {userAuthenticated && ( - - )}{' '} - {editable && isEditing && ( - - )} + {!expanded && !isEditing && valid && showMetadataToggle && ( + onMetadataChangeFold(checked)} + style={switchMarginRight} + /> + )} + {showExpanded && !isEditing && valid && ( + onFormatChangeFold(expanded)} + style={switchMarginRight} + /> + )} + {userAuthenticated && ( + + )}{' '} + {editable && isEditing && ( + + )} +
)} @@ -283,7 +290,7 @@ const ResourceEditor: React.FunctionComponent = props => { handleChange={handleChange} keyFoldCode={keyFoldCode} onLinksFound={onLinksFound} - fullscreen={limited} + fullscreen={fullscreen} />
); diff --git a/src/shared/containers/ResourceEditor.tsx b/src/shared/containers/ResourceEditor.tsx index 26de03c9c..dbbbe57ab 100644 --- a/src/shared/containers/ResourceEditor.tsx +++ b/src/shared/containers/ResourceEditor.tsx @@ -13,7 +13,7 @@ import ResourceEditor from '../components/ResourceEditor'; import { getDataExplorerResourceItemArray } from '../components/ResourceEditor/editorUtils'; import useNotification, { parseNexusError } from '../hooks/useNotification'; import { - InitDataExplorerGraphFlowLimitedVersion, + InitDataExplorerGraphFlowFullscreenVersion, InitNewVisitDataExplorerGraphView, } from '../store/reducers/data-explorer'; import { @@ -129,7 +129,9 @@ const ResourceEditorContainer: React.FunctionComponent<{ )) as Resource; const orgProject = getOrgAndProjectFromResourceObject(data); if (location.pathname === '/data-explorer/graph-flow') { - dispatch(InitDataExplorerGraphFlowLimitedVersion(true)); + dispatch( + InitDataExplorerGraphFlowFullscreenVersion({ fullscreen: true }) + ); } else { dispatch( InitNewVisitDataExplorerGraphView({ @@ -143,7 +145,7 @@ const ResourceEditorContainer: React.FunctionComponent<{ data ), }, - limited: true, + fullscreen: true, }) ); navigate.push('/data-explorer/graph-flow'); diff --git a/src/shared/organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent.tsx b/src/shared/organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent.tsx index f644be773..8978494c6 100644 --- a/src/shared/organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowContent/DataExplorerGraphFlowContent.tsx @@ -3,18 +3,18 @@ import { useSelector } from 'react-redux'; import { RootState } from '../../store/reducers'; import ResourceViewContainer from '../../containers/ResourceViewContainer'; import ResourceEditorContainer from '../../containers/ResourceEditor'; -import { DataExplorerGraphFlowContentLimitedHeader } from '../../molecules/DataExplorerGraphFlowMolecules'; +import { DEFGContentFullscreenHeader } from '../../molecules/DataExplorerGraphFlowMolecules'; import './styles.less'; const DataExplorerContentPage = ({}) => { - const { current, limited } = useSelector( + const { current, fullscreen } = useSelector( (state: RootState) => state.dataExplorer ); return (
- {limited ? ( + {fullscreen ? ( - + {}} From 00ba83d41692f63693461d83cc3294e40a4e8c22 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 13:05:46 +0200 Subject: [PATCH 140/192] f-4047/update: refactor resource constructor in resolution --- .../ResourceEditor/useResolutionActions.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/shared/components/ResourceEditor/useResolutionActions.tsx b/src/shared/components/ResourceEditor/useResolutionActions.tsx index 5f1772589..5512a1260 100644 --- a/src/shared/components/ResourceEditor/useResolutionActions.tsx +++ b/src/shared/components/ResourceEditor/useResolutionActions.tsx @@ -14,6 +14,7 @@ import { } from '../../utils'; import { parseResourceId } from '../Preview/Preview'; import { download } from '../../utils/download'; +import { getDataExplorerResourceItemArray } from './editorUtils'; const useResolvedLinkEditorPopover = () => { const nexus = useNexusContext(); @@ -44,11 +45,13 @@ const useResolvedLinkEditorPopover = () => { _self: data._self, title: getResourceLabel(data), types: getNormalizedTypes(data['@type']), - resource: [ - orgProject?.orgLabel ?? '', - orgProject?.projectLabel ?? '', - data['@id'], - ], + resource: getDataExplorerResourceItemArray( + { + orgLabel: orgProject?.orgLabel ?? '', + projectLabel: orgProject?.projectLabel ?? '', + }, + data + ), }, current: resource, }) From 51411e0d8b1b2a29ed5a0ab932c7d7cffce05180 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 13:09:59 +0200 Subject: [PATCH 141/192] f-4047/update: the style of resource view when there is no backgroud history route --- src/shared/views/ResourceView.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/shared/views/ResourceView.tsx b/src/shared/views/ResourceView.tsx index f7aa9a1da..0e07ded51 100644 --- a/src/shared/views/ResourceView.tsx +++ b/src/shared/views/ResourceView.tsx @@ -1,9 +1,18 @@ import * as React from 'react'; +import { useHistory } from 'react-router'; +import { clsx } from 'clsx'; import ResourceViewContainer from '../containers/ResourceViewContainer'; -const ResourceView: React.FunctionComponent = props => { +const ResourceView: React.FunctionComponent = () => { + const { location } = useHistory(); + const background = !!(location.state as any)?.background; return ( -
+
); From a9ec457e8969e8c71443b05ef6452fe762707c57 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 13:10:50 +0200 Subject: [PATCH 142/192] f-4047/ref: degf-fullscreen header export --- src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts index e1fc1cd80..9fe3f1bd3 100644 --- a/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts +++ b/src/shared/molecules/DataExplorerGraphFlowMolecules/index.ts @@ -1,4 +1,4 @@ -export { default as DataExplorerGraphFlowContentLimitedHeader } from './ContentLimitedHeader'; +export { default as DEFGContentFullscreenHeader } from './ContentFullscreenHeader'; export { default as NavigationArrow } from './NavigationArrow'; export { default as NavigationStackItem } from './NavigationStackItem'; export { default as NavigationStackShrinkedItem } from './NavigationStackShrinkedItem'; From fd3b3827e4e1168dae63ad49237a557baf0c37d1 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 13:11:16 +0200 Subject: [PATCH 143/192] f-4047/update: tests for the new header and use the new fullscreen state --- .../DateExplorerGraphFlow.spec.tsx | 91 ++++++++++++------- .../NavigationArrows.spec.tsx | 4 +- .../NavigationStack.spec.tsx | 2 +- 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx index 9d812189b..6a0511cdc 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.spec.tsx @@ -1,6 +1,6 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { waitFor } from '@testing-library/react'; +import { RenderResult, waitFor } from '@testing-library/react'; import { Provider } from 'react-redux'; import { NexusClient, createNexusClient } from '@bbp/nexus-sdk'; import { AnyAction, Store } from 'redux'; @@ -9,10 +9,11 @@ import { createMemoryHistory, MemoryHistory } from 'history'; import { Router } from 'react-router-dom'; import { setupServer } from 'msw/node'; import { deltaPath } from '__mocks__/handlers/handlers'; -import { cleanup, render, act, screen } from '../../../utils/testUtil'; +import { cleanup, render, screen } from '../../../utils/testUtil'; import { DATA_EXPLORER_GRAPH_FLOW_DIGEST, InitNewVisitDataExplorerGraphView, + TDataExplorerState, } from '../../../shared/store/reducers/data-explorer'; import configureStore from '../../store'; import DateExplorerGraphFlow from './DateExplorerGraphFlow'; @@ -21,8 +22,10 @@ import { getDataExplorerGraphFlowResourceObject, getDataExplorerGraphFlowResourceObjectTags, } from '../../../__mocks__/handlers/DataExplorerGraphFlow/handlers'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; +import userEvent from '@testing-library/user-event'; -const initialDataExplorerState = { +const initialDataExplorerState: TDataExplorerState = { current: { isDownloadable: false, _self: initialResource._self, @@ -30,16 +33,22 @@ const initialDataExplorerState = { types: initialResource['@type'], resource: ['public', 'sscx', initialResource['@id'], initialResource._rev], }, - links: [], - shrinked: false, - highlightIndex: -1, - limited: false, + leftNodes: { links: [], shrinked: false }, + rightNodes: { links: [], shrinked: false }, + fullscreen: false, }; + describe('DataExplorerGraphFlow', () => { + let server: ReturnType; + let app: JSX.Element; + let container: HTMLElement; + let rerender: (ui: React.ReactElement) => void; let store: Store; + let user: UserEvent; let history: MemoryHistory<{}>; - let server: ReturnType; let nexus: NexusClient; + let component: RenderResult; + beforeAll(async () => { nexus = createNexusClient({ fetch, @@ -78,8 +87,15 @@ describe('DataExplorerGraphFlow', () => { cleanup(); }); - it('should render the name of the resource', async () => { - const App: JSX.Element = ( + beforeEach(() => { + history = createMemoryHistory({}); + + nexus = createNexusClient({ + fetch, + uri: deltaPath(), + }); + store = configureStore(history, { nexus }, {}); + app = ( @@ -88,49 +104,62 @@ describe('DataExplorerGraphFlow', () => { ); - await act(async () => { - await render(App); - }); + component = render(app); + container = component.container; + rerender = component.rerender; + user = userEvent.setup(); + }); + + it('should render the name of the resource', async () => { store.dispatch( InitNewVisitDataExplorerGraphView({ current: initialDataExplorerState.current, - limited: false, + fullscreen: false, }) ); - + rerender(app); const resourceTitle = await waitFor(() => screen.getByText(initialResource.name) ); expect(resourceTitle).toBeInTheDocument(); }); it('should clean the data explorer state when quit the page', async () => { - const App: JSX.Element = ( - - - - - - - - ); - await act(async () => { - await render(App); - }); store.dispatch( InitNewVisitDataExplorerGraphView({ current: initialDataExplorerState.current, - limited: false, + fullscreen: false, }) ); + rerender(app); history.push('/another-page'); const dataExplorerState = store.getState().dataExplorer; - const localstorage = sessionStorage.getItem( + const sessionStorageItem = sessionStorage.getItem( DATA_EXPLORER_GRAPH_FLOW_DIGEST ); - expect(localstorage).toBeNull(); + expect(sessionStorageItem).toBeNull(); expect(dataExplorerState.leftNodes.links.length).toBe(0); expect(dataExplorerState.rightNodes.links.length).toBe(0); expect(dataExplorerState.current).toBeNull(); - expect(dataExplorerState.limited).toBe(false); + expect(dataExplorerState.fullscreen).toBe(false); + }); + + it('should the fullscren toggle present in the screen if the user in fullscreen mode', async () => { + store.dispatch( + InitNewVisitDataExplorerGraphView({ + current: initialDataExplorerState.current, + fullscreen: true, + }) + ); + rerender(app); + const fullscreenSwitch = container.querySelector( + 'button[aria-label="fullscreen switch"]' + ); + const fullscreenTitle = container.querySelector( + 'h1[aria-label="fullscreen title"]' + ); + expect(fullscreenSwitch).toBeInTheDocument(); + expect(fullscreenTitle).toBeInTheDocument(); + await user.click(fullscreenSwitch as HTMLButtonElement); + expect(store.getState().dataExplorer.fullscreen).toBe(false); }); }); diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.spec.tsx index 40fed18b2..d6a401c44 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationArrows.spec.tsx @@ -12,9 +12,7 @@ import { NexusProvider } from '@bbp/react-nexus'; import { deltaPath } from '../../../__mocks__/handlers/handlers'; import configureStore from '../../store'; import { - AddNewNodeDataExplorerGraphFlow, ResetDataExplorerGraphFlow, - PopulateDataExplorerGraphFlow, TDataExplorerState, } from '../../store/reducers/data-explorer'; import NavigationArrows from './NavigationArrows'; @@ -108,7 +106,7 @@ const initialDataExplorerState: TDataExplorerState = { ], shrinked: false, }, - limited: false, + fullscreen: false, referer: { pathname: '/my-data', search: '', diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx index cb9238e1d..044951630 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/NavigationStack.spec.tsx @@ -73,7 +73,7 @@ const initialDataExplorerState: TDataExplorerState = { links: [], shrinked: false, }, - limited: false, + fullscreen: false, }; const fourthItemInStack = { isDownloadable: false, From fe3916a49136559e2ebdc90a1385b62a211cdd05 Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Mon, 10 Jul 2023 11:14:41 +0200 Subject: [PATCH 144/192] US 4051 // Filter data explorer rows by type Signed-off-by: Dinika Saxena --- .../handlers/DataExplorer/handlers.ts | 189 ++++++++++++------ .../dataExplorer/DataExplorer.spec.tsx | 75 ++++++- src/subapps/dataExplorer/DataExplorer.tsx | 14 +- .../dataExplorer/DataExplorerUtils.tsx | 45 +++++ src/subapps/dataExplorer/ProjectSelector.tsx | 72 +++---- src/subapps/dataExplorer/TypeSelector.tsx | 98 +++++++++ src/subapps/dataExplorer/styles.less | 22 +- 7 files changed, 402 insertions(+), 113 deletions(-) create mode 100644 src/subapps/dataExplorer/DataExplorerUtils.tsx create mode 100644 src/subapps/dataExplorer/TypeSelector.tsx diff --git a/src/__mocks__/handlers/DataExplorer/handlers.ts b/src/__mocks__/handlers/DataExplorer/handlers.ts index 5e6625ee7..60d9597bf 100644 --- a/src/__mocks__/handlers/DataExplorer/handlers.ts +++ b/src/__mocks__/handlers/DataExplorer/handlers.ts @@ -1,12 +1,20 @@ import { rest } from 'msw'; import { deltaPath } from '__mocks__/handlers/handlers'; import { Project, Resource } from '@bbp/nexus-sdk'; +import { + AggregatedBucket, + AggregationsResult, +} from 'subapps/dataExplorer/DataExplorerUtils'; export const dataExplorerPageHandler = ( mockResources: Resource[], total: number = 300 -) => - rest.get(deltaPath(`/resources`), (req, res, ctx) => { +) => { + return rest.get(deltaPath(`/resources`), (req, res, ctx) => { + if (req.url.searchParams.has('aggregations')) { + return res(ctx.status(200), ctx.json(mockAggregationsResult())); + } + const passedType = req.url.searchParams.get('type'); const mockResponse = { '@context': [ 'https://bluebrain.github.io/nexus/contexts/metadata.json', @@ -14,23 +22,41 @@ export const dataExplorerPageHandler = ( 'https://bluebrain.github.io/nexus/contexts/search-metadata.json', ], _total: total, - _results: mockResources, + _results: passedType + ? mockResources.filter(res => res['@type'] === passedType) + : mockResources, _next: 'https://bbp.epfl.ch/nexus/v1/resources?size=50&sort=@id&after=%5B1687269183553,%22https://bbp.epfl.ch/neurosciencegraph/data/31e22529-2c36-44f0-9158-193eb50526cd%22%5D', }; return res(ctx.status(200), ctx.json(mockResponse)); }); +}; + +export const filterByProjectHandler = (mockResources: Resource[]) => { + return rest.get(deltaPath(`/resources/:org/:project`), (req, res, ctx) => { + if (req.url.searchParams.has('aggregations')) { + return res( + ctx.status(200), + ctx.json( + mockAggregationsResult([ + getMockTypesBucket( + 'https://bluebrain.github.io/nexus/vocabulary/File' + ), + getMockTypesBucket('http://schema.org/StudioDashboard'), + getMockTypesBucket('https://neuroshapes.org/NeuronMorphology'), + ]) + ) + ); + } -export const filterByProjectHandler = (mockReources: Resource[]) => - rest.get(deltaPath(`/resources/:org/:project`), (req, res, ctx) => { const { project } = req.params; const responseBody = project - ? mockReources.filter( + ? mockResources.filter( res => res._project.slice(res._project.lastIndexOf('/') + 1) === project ) - : mockReources; + : mockResources; const mockResponse = { '@context': [ 'https://bluebrain.github.io/nexus/contexts/metadata.json', @@ -44,69 +70,102 @@ export const filterByProjectHandler = (mockReources: Resource[]) => }; return res(ctx.status(200), ctx.json(mockResponse)); }); +}; -export const getProjectHandler = () => - rest.get(deltaPath(`/projects`), (req, res, ctx) => { - const projectResponse = { - '@context': [ - 'https://bluebrain.github.io/nexus/contexts/metadata.json', - 'https://bluebrain.github.io/nexus/contexts/search.json', - 'https://bluebrain.github.io/nexus/contexts/projects.json', - ], - _next: - 'https://bbp.epfl.ch/nexus/v1/projects?from=10&label=&size=10&sort=_label', - _total: 10, - _results: [ - getMockProject('something-brainy', 'bbp'), - - getMockProject('smarty', 'bbp'), +const mockAggregationsResult = ( + bucketForTypes: AggregatedBucket[] = defaultBucketForTypes +): AggregationsResult => { + return { + '@context': 'https://bluebrain.github.io/nexus/contexts/aggregations.json', + total: 10, + aggregations: { + projects: { + buckets: [ + getMockProjectBucket('something-brainy', 'bbp'), + getMockProjectBucket('smarty', 'bbp'), + getMockProjectBucket('unhcr', 'un'), + getMockProjectBucket('unicef', 'un'), + getMockProjectBucket('tellytubbies', 'bbc'), + ], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + }, + types: { + buckets: bucketForTypes, + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + }, + }, + }; +}; - getMockProject('unhcr', 'un'), - getMockProject('unicef', 'un'), - getMockProject('tellytubbies', 'bbc'), - ], +export const getAggregationsHandler = () => + rest.get(deltaPath(`/resources?aggregations=true`), (req, res, ctx) => { + const aggregationsResponse: AggregationsResult = { + '@context': + 'https://bluebrain.github.io/nexus/contexts/aggregations.json', + total: 10, + aggregations: { + projects: { + buckets: [ + getMockProjectBucket('something-brainy', 'bbp'), + getMockProjectBucket('smarty', 'bbp'), + getMockProjectBucket('unhcr', 'un'), + getMockProjectBucket('unicef', 'un'), + getMockProjectBucket('tellytubbies', 'bbc'), + ], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + }, + types: { + buckets: [ + getMockProjectBucket('something-brainy', 'bbp'), + getMockProjectBucket('smarty', 'bbp'), + getMockProjectBucket('unhcr', 'un'), + getMockProjectBucket('unicef', 'un'), + getMockProjectBucket('tellytubbies', 'bbc'), + ], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + }, + }, }; - return res(ctx.status(200), ctx.json(projectResponse)); + return res(ctx.status(200), ctx.json(aggregationsResponse)); }); -const getMockProject = (project: string, org: string = 'bbp'): Project => { +const getMockProjectBucket = (project: string, org: string = 'bbp') => { return { - '@id': `https://bbp.epfl.ch/nexus/v1/projects/bbp/${project}`, - '@type': 'Project', - apiMappings: [ - { - namespace: 'https://neuroshapes.org/dash/', - prefix: 'datashapes', - }, - ], - '@context': ['mockcontext'], - base: 'https://bbp.epfl.ch/neurosciencegraph/data/', - description: 'This is such a dumb mock project. dumb dumb dumb.', - vocab: - 'https://bbp.epfl.ch/nexus/v1/resources/bbp/Blue-Brain-Ketogenic-Project-(BBK)/_/', - _constrainedBy: 'https://bluebrain.github.io/nexus/schemas/projects.json', - _createdAt: '2021-03-04T21:27:18.900Z', - _createdBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/harikris', - _deprecated: true, - _label: `${project}`, - _organizationLabel: org, - _organizationUuid: 'a605b71a-377d-4df3-95f8-923149d04106', - _rev: 2, - _self: `https://bbp.epfl.ch/nexus/v1/projects/bbp/${project}`, - _updatedAt: '2021-03-15T09:05:05.882Z', - _updatedBy: 'https://bbp.epfl.ch/nexus/v1/realms/bbp/users/harikris', + key: `https://bbp.epfl.ch/nexus/v1/projects/${org}/${project}`, + doc_count: 10, }; }; +const getMockTypesBucket = (type: string) => { + return { + doc_count: 98, + key: type, + }; +}; + +const defaultBucketForTypes = [ + getMockTypesBucket('https://bluebrain.github.io/nexus/vocabulary/File'), + getMockTypesBucket('http://schema.org/Dataset'), + getMockTypesBucket('https://neuroshapes.org/NeuronMorphology'), + getMockTypesBucket('https://bluebrain.github.io/nexus/vocabulary/View'), + getMockTypesBucket( + 'https://bluebrainnexus.io/studio/vocabulary/StudioDashboard' + ), +]; + export const getMockResource = ( selfSuffix: string, extra: { [key: string]: any }, - project: string = 'hippocampus' + project: string = 'hippocampus', + type: string = 'https://bbp.epfl.ch/ontologies/core/bmo/SimulationCampaignConfiguration' ): Resource => ({ ...extra, '@id': `https://bbp.epfl.ch/neurosciencegraph/data/${selfSuffix}`, - '@type': - 'https://bbp.epfl.ch/ontologies/core/bmo/SimulationCampaignConfiguration', + '@type': type, _constrainedBy: 'https://bluebrain.github.io/nexus/schemas/unconstrained.json', _createdAt: '2023-06-21T09:39:47.217Z', @@ -123,15 +182,29 @@ export const getMockResource = ( export const defaultMockResult: Resource[] = [ getMockResource('self1', {}), - getMockResource('self2', { specialProperty: 'superSpecialValue' }, 'unhcr'), + getMockResource( + 'self2', + { specialProperty: 'superSpecialValue' }, + 'unhcr', + 'https://bluebrain.github.io/nexus/vocabulary/File' + ), getMockResource('self3', { specialProperty: ['superSpecialValue'] }), - getMockResource('self4', { specialProperty: '' }), + getMockResource( + 'self4', + { specialProperty: '' }, + 'https://bluebrain.github.io/nexus/vocabulary/File' + ), getMockResource('self5', { specialProperty: [] }), getMockResource('self6', { specialProperty: ['superSpecialValue', 'so'], }), getMockResource('self7', { specialProperty: { foo: 1, bar: 2 } }, 'unhcr'), getMockResource('self8', { specialProperty: null }), - getMockResource('self9', { specialProperty: {} }), + getMockResource( + 'self9', + { specialProperty: {} }, + undefined, + 'https://bluebrain.github.io/nexus/vocabulary/File' + ), getMockResource('self10', { specialProperty: undefined }), ]; diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 021e2c42c..a88c8a032 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -9,7 +9,6 @@ import { defaultMockResult, filterByProjectHandler, getMockResource, - getProjectHandler, } from '__mocks__/handlers/DataExplorer/handlers'; import { deltaPath } from '__mocks__/handlers/handlers'; import { setupServer } from 'msw/node'; @@ -36,8 +35,7 @@ describe('DataExplorer', () => { const server = setupServer( dataExplorerPageHandler(defaultMockResult, defaultTotalResults), - filterByProjectHandler(defaultMockResult), - getProjectHandler() + filterByProjectHandler(defaultMockResult) ); const history = createMemoryHistory({}); @@ -85,9 +83,12 @@ describe('DataExplorer', () => { const DropdownSelector = '.ant-select-dropdown'; const DropdownOptionSelector = 'div.ant-select-item-option-content'; + const TypeOptionSelector = 'div.ant-select-item-option-content > span'; + const PathMenuLabel = 'path-selector'; const PredicateMenuLabel = 'predicate-selector'; const ProjectMenuLabel = 'project-filter'; + const TypeMenuLabel = 'type-filter'; const expectRowCountToBe = async (expectedRowsCount: number) => { return await waitFor(() => { @@ -162,6 +163,11 @@ describe('DataExplorer', () => { return projectColumn?.textContent; }; + const typeFromRow = (row: Element) => { + const typeColumn = row.querySelectorAll('td')[1]; // second column is the type column + return typeColumn?.textContent; + }; + const visibleTableRows = () => { return container.querySelectorAll('table tbody tr.data-explorer-row'); }; @@ -172,9 +178,12 @@ describe('DataExplorer', () => { }); }; - const getDropdownOption = async (optionLabel: string) => - await screen.getByText(new RegExp(`${optionLabel}$`), { - selector: DropdownOptionSelector, + const getDropdownOption = async ( + optionLabel: string, + selector: string = DropdownOptionSelector + ) => + await screen.getByText(new RegExp(`${optionLabel}$`, 'i'), { + selector, }); const getRowsForNextPage = async ( @@ -206,10 +215,11 @@ describe('DataExplorer', () => { const selectOptionFromMenu = async ( menuAriaLabel: string, - optionLabel: string + optionLabel: string, + optionSelector?: string ) => { await openMenuFor(menuAriaLabel); - const option = await getDropdownOption(optionLabel); + const option = await getDropdownOption(optionLabel, optionSelector); await userEvent.click(option, { pointerEventsCheck: 0 }); }; @@ -260,6 +270,10 @@ describe('DataExplorer', () => { await expectRowCountToBe(resources.length); }; + const getResetProjectButton = async () => { + return await screen.getByTestId('reset-project-button'); + }; + it('shows rows for all fetched resources', async () => { await expectRowCountToBe(10); }); @@ -417,8 +431,31 @@ describe('DataExplorer', () => { visibleTableRows().forEach(row => expect(projectFromRow(row)).toMatch(/unhcr/i) ); + }); + + it('resets selected project when user clicks reset button', async () => { + await expectRowCountToBe(10); + + await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); + + expect(visibleTableRows().length).toBeLessThan(10); + + const resetProjectButton = await getResetProjectButton(); + await userEvent.click(resetProjectButton); + await expectRowCountToBe(10); + }); + + it('shows all projects when allProjects option is selected', async () => { + await expectRowCountToBe(10); + await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); + + expect(visibleTableRows().length).toBeLessThan(10); + + const resetProjectButton = await getResetProjectButton(); + await userEvent.click(resetProjectButton); await selectOptionFromMenu(ProjectMenuLabel, AllProjects); + await expectRowCountToBe(10); }); @@ -430,6 +467,28 @@ describe('DataExplorer', () => { await expectProjectOptionsToMatch('bbc'); }); + it('shows resources filtered by the selected type', async () => { + await expectRowCountToBe(10); + await selectOptionFromMenu(TypeMenuLabel, 'file', TypeOptionSelector); + + visibleTableRows().forEach(row => + expect(typeFromRow(row)).toMatch(/file/i) + ); + }); + + it('only shows types that exist in selected project in type autocomplete', async () => { + await openMenuFor(TypeMenuLabel); + const optionBefore = await getDropdownOption('Dataset', TypeOptionSelector); + expect(optionBefore).toBeInTheDocument(); + + await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); + + await openMenuFor(TypeMenuLabel); + expect( + getDropdownOption('Dataset', TypeOptionSelector) + ).rejects.toThrowError(); + }); + it('shows paths as options in a select menu of path selector', async () => { await openMenuFor('path-selector'); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 95feab006..9a001daa3 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -9,11 +9,13 @@ import './styles.less'; import { ProjectSelector } from './ProjectSelector'; import { PredicateSelector } from './PredicateSelector'; import { DatasetCount } from './DatasetCount'; +import { TypeSelector } from './TypeSelector'; export interface DataExplorerConfiguration { pageSize: number; offset: number; orgAndProject?: [string, string]; + type: string | undefined; predicateFilter: ((resource: Resource) => boolean) | null; } @@ -21,7 +23,7 @@ export const DataExplorer: React.FC<{}> = () => { const nexus = useNexusContext(); const [ - { pageSize, offset, orgAndProject, predicateFilter }, + { pageSize, offset, orgAndProject, predicateFilter, type }, updateTableConfiguration, ] = useReducer( ( @@ -32,15 +34,17 @@ export const DataExplorer: React.FC<{}> = () => { pageSize: 50, offset: 0, orgAndProject: undefined, + type: undefined, predicateFilter: null, } ); const { data: resources, isLoading } = useQuery({ - queryKey: ['data-explorer', { pageSize, offset, orgAndProject }], + queryKey: ['data-explorer', { pageSize, offset, orgAndProject, type }], retry: false, queryFn: () => { return nexus.Resource.list(orgAndProject?.[0], orgAndProject?.[1], { + type, from: offset, size: pageSize, }); @@ -84,6 +88,12 @@ export const DataExplorer: React.FC<{}> = () => { } }} /> + { + updateTableConfiguration({ type: selectedType }); + }} + /> { + const nexus = useNexusContext(); + return useQuery({ + queryKey: ['data-explorer-aggregations', orgAndProject], + retry: false, + queryFn: async () => { + return await nexus.Resource.list(orgAndProject?.[0], orgAndProject?.[1], { + aggregations: true, + }); + }, + select: data => { + return ( + ((data as unknown) as AggregationsResult).aggregations[bucketName] + ?.buckets ?? ([] as AggregatedBucket[]) + ); + }, + onError: error => { + notification.error({ message: 'Aggregations could not be fetched' }); + }, + }); +}; + +export interface AggregationsResult { + '@context': string; + total: number; + aggregations: { + projects: AggregatedProperty; + types: AggregatedProperty; + }; +} + +export type AggregatedProperty = { + buckets: AggregatedBucket[]; + doc_count_error_upper_bound: number; + sum_other_doc_count: number; +}; + +export type AggregatedBucket = { key: string; doc_count: number }; diff --git a/src/subapps/dataExplorer/ProjectSelector.tsx b/src/subapps/dataExplorer/ProjectSelector.tsx index e1e19e0bc..173ad87e7 100644 --- a/src/subapps/dataExplorer/ProjectSelector.tsx +++ b/src/subapps/dataExplorer/ProjectSelector.tsx @@ -1,47 +1,61 @@ -import { ProjectResponseCommon } from '@bbp/nexus-sdk'; -import { useNexusContext } from '@bbp/react-nexus'; -import { AutoComplete, Input, notification } from 'antd'; +import { AutoComplete, Input } from 'antd'; import React, { useState } from 'react'; -import { useQuery } from 'react-query'; +import { makeOrgProjectTuple } from '../../shared/molecules/MyDataTable/MyDataTable'; +import { AggregatedBucket, useAggregations } from './DataExplorerUtils'; import './styles.less'; +import { CloseOutlined, SearchOutlined } from '@ant-design/icons'; +import { normalizeString } from '../../utils/stringUtils'; interface Props { onSelect: (orgLabel?: string, projectLabel?: string) => void; } export const ProjectSelector: React.FC = ({ onSelect }: Props) => { - const { data: projects } = useProjects(); - const [searchTerm, setSearchTerm] = useState(''); + const { data: projects } = useAggregations('projects'); + const [showClearIcon, setShowClearIcon] = useState(false); const allOptions = [ { value: AllProjects, label: AllProjects }, ...(projects?.map(projectToOption) ?? []), - ].filter(project => project.value.includes(searchTerm)); + ]; return (
- Show me + Project: { - setSearchTerm(normalizeSearchTerm(text)); + setShowClearIcon(text ? true : false); + }} + filterOption={(searchTerm, option) => { + if (!option) { + return false; + } + return normalizeString(option.value).includes(searchTerm); }} onSelect={text => { if (text === AllProjects) { + setShowClearIcon(false); onSelect(undefined, undefined); } else { const [org, project] = text.split('/'); + setShowClearIcon(true); onSelect(org, project); } }} + allowClear={true} + clearIcon={} + onClear={() => onSelect(undefined, undefined)} + placeholder={AllProjects} aria-label="project-filter" bordered={false} className="search-input" popupClassName="search-menu" > - + : } + />
); @@ -50,37 +64,11 @@ export const ProjectSelector: React.FC = ({ onSelect }: Props) => { export const AllProjects = 'All Projects'; const projectToOption = ( - project: ProjectResponseCommon + projectBucket: AggregatedBucket ): { value: string; label: string } => { + const { org, project } = makeOrgProjectTuple(projectBucket.key); return { - value: `${project._organizationLabel}/${project._label}`, - label: `${project._organizationLabel}/${project._label}`, + value: `${org}/${project}`, + label: `${org}/${project}`, }; }; - -const useProjects = () => { - const nexus = useNexusContext(); - return useQuery({ - queryKey: ['data-explorer-projects'], - retry: false, - queryFn: async () => { - // TODO: Replace this with aggregation API when it is ready. - // NOTE: At the moment it is not possible to get all projects a user has access to in 1 request. To get around this, we make 2 requests: - // 1st request -> Get the total number of projects (n) a user can see - // 2nd request -> Use the total retrieved from above request to specify the size of the projects to return. - const firstPageOfProjects = await nexus.Project.list(undefined, { - size: 1, - }); - const allProjects = await nexus.Project.list(undefined, { - size: firstPageOfProjects._total, - }); - - return allProjects._results; - }, - onError: error => { - notification.error({ message: 'Projects could not be fetched' }); - }, - }); -}; - -const normalizeSearchTerm = (text: string) => text.trim().toLowerCase(); diff --git a/src/subapps/dataExplorer/TypeSelector.tsx b/src/subapps/dataExplorer/TypeSelector.tsx new file mode 100644 index 000000000..4d04b058b --- /dev/null +++ b/src/subapps/dataExplorer/TypeSelector.tsx @@ -0,0 +1,98 @@ +import { CloseOutlined, SearchOutlined } from '@ant-design/icons'; +import * as Sentry from '@sentry/browser'; +import { isString } from 'lodash'; +import React, { useEffect, useRef, useState } from 'react'; +import { normalizeString } from '../../utils/stringUtils'; +import isValidUrl from '../../utils/validUrl'; +import { AggregatedBucket, useAggregations } from './DataExplorerUtils'; +import './styles.less'; +import Select, { DefaultOptionType } from 'antd/lib/select'; + +interface Props { + orgAndProject?: string[]; + onSelect: (type: string | undefined) => void; +} + +export const TypeSelector: React.FC = ({ + onSelect, + orgAndProject, +}: Props) => { + const { data: aggregatedTypes, isSuccess } = useAggregations( + 'types', + orgAndProject + ); + const [showClearIcon, setShowClearIcon] = useState(false); + const allOptions = [...(aggregatedTypes?.map(typeToOption) ?? [])]; + const [displayedOptions, setDisplayedOptions] = useState(allOptions); + + const optionsRef = useRef(allOptions); + + useEffect(() => { + if (isSuccess) { + optionsRef.current = [...(aggregatedTypes?.map(typeToOption) ?? [])]; + setDisplayedOptions(optionsRef.current); + } + }, [isSuccess, aggregatedTypes]); + + return ( +
+ Type: + +
+ ); +}; + +const typeToOption = (typeBucket: AggregatedBucket): TypeOption => { + const typeKey = typeBucket.key; + + const typeLabel = + isString(typeKey) && isValidUrl(typeKey) + ? typeKey.split('/').pop() + : typeKey; + + if (!typeLabel) { + Sentry.captureException('Invalid type received from delta', { + extra: { + typeBucket, + }, + }); + } + + return { + value: typeKey, + label: {typeLabel}, + key: typeKey, + id: typeKey, + }; +}; + +type TypeOption = DefaultOptionType; diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index d39b5f1cf..e162e228a 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -42,17 +42,33 @@ .search-input { border-bottom: 1px solid @fusion-neutral-7; margin-left: 8px !important; + color: @fusion-blue-8 !important; + width: 200px; input { color: @fusion-blue-8 !important; border: none; background: @fusion-main-bg; font-weight: 700; + + &::placeholder { + color: @fusion-neutral-7; + font-weight: 400; + } + } + + input:focus { + border: none !important; } - button { - background: #f5f5f5; - border: unset; + .ant-input-group-addon { + background: transparent; + border: none; + } + + .anticon-search, + .anticon-close { + color: @fusion-blue-8; } } From f26f55ebe70a798d2c19fee2ffc8e109f3433aa0 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Thu, 13 Jul 2023 17:40:01 +0200 Subject: [PATCH 145/192] f-4048/update: save json update only when resources/write permission present --- src/shared/components/ResourceEditor/index.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index cff247452..2d230fbee 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -7,7 +7,7 @@ import { SaveOutlined, } from '@ant-design/icons'; import { useSelector } from 'react-redux'; -import { useNexusContext } from '@bbp/react-nexus'; +import { AccessControl } from '@bbp/react-nexus'; import codemiror from 'codemirror'; import 'codemirror/mode/javascript/javascript'; @@ -261,7 +261,11 @@ const ResourceEditor: React.FunctionComponent = props => { style={switchMarginRight} /> )} - {userAuthenticated && ( + <>} + > - )}{' '} + {editable && isEditing && (
+ } + > + } - onClick={() => setOpenDateFilterContainer(state => !state)} - onChange={(e: React.ChangeEvent) => { - if (e.type === 'click') { - updateCurrentDates({ - dateFilterType: undefined, - singleDate: undefined, - dateStart: undefined, - dateEnd: undefined, - }); - setFilterOptions({ - dateFilterType: undefined, - singleDate: undefined, - dateStart: undefined, - dateEnd: undefined, - }); - } - }} - /> - - {/* -
- -
{typesDataSources.length} types total
-
-
- {typesDataSources && - typesDataSources.map((tp: TType) => { - return ( - - {startCase(tp.label)} - - handleOnCheckType(e, tp)} - checked={dataType.includes(tp.key)} - /> - - - ); - })} -
-
- } - > - startCase(item.split('/').pop()))} - /> - */} -
- -
- - By resource id or self - -
-
+ {text} + {total ? `${prettifyNumber(total)} ${label}` : ''}
); }; const MyDataHeader: React.FC = ({ total, - dataType, + types, dateField, query, setFilterOptions, @@ -439,13 +28,11 @@ const MyDataHeader: React.FC = ({ - <Filters + <MyDataHeaderFilters {...{ - dataType, + types, dateField, query, locate, diff --git a/src/shared/molecules/MyDataHeader/styles.less b/src/shared/molecules/MyDataHeader/styles.less index 987c40a3f..4c60abf39 100644 --- a/src/shared/molecules/MyDataHeader/styles.less +++ b/src/shared/molecules/MyDataHeader/styles.less @@ -2,9 +2,9 @@ .my-data-table-header { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; - flex-wrap: wrap; + flex-direction: column; &-title { user-select: none; @@ -33,10 +33,11 @@ } &-actions { + width: 100%; margin: 0; display: grid; gap: 15px; - grid-template-columns: max-content max-content 300px 1fr; + grid-template-columns: max-content max-content 300px 300px 1fr; align-items: center; justify-content: center; align-content: center; @@ -137,8 +138,22 @@ .ant-select-selection-placeholder { color: @fusion-daybreak-10 !important; } + .ant-select-selection-overflow { + flex-wrap: nowrap; + overflow: scroll; + // style the scrollbar of this ant-select-selection-overflow + &::-webkit-scrollbar { + width: 2px; + height: 2px; + } + &::-webkit-scrollbar-thumb { + background: @fusion-daybreak-10; + } + } +} +.my-data-type-picker-popup { + display: none; } - .my-data-type-filter { &-popover { background-color: white !important; @@ -194,6 +209,17 @@ -ms-overflow-style: none; scrollbar-width: none; + .no-types-content { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + span { + user-select: none; + font-size: 12px; + color: #8c8c8c; + } + } } } @@ -228,6 +254,14 @@ outline: none; box-shadow: none; } + .ant-input-affix-wrapper { + border: none; + background-color: transparent; + &-focused { + box-shadow: none; + outline: none; + } + } } .my-data-type-picker { From 004d91dcbd8a9049ce0e6546263d14b95db44756 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Sun, 16 Jul 2023 11:08:52 +0200 Subject: [PATCH 157/192] f-3945/ref: my-data --- src/pages/MyDataPage/MyDataPage.tsx | 6 +-- src/shared/canvas/MyData/MyData.tsx | 59 ++++++---------------------- src/shared/canvas/MyData/styles.less | 6 ++- 3 files changed, 16 insertions(+), 55 deletions(-) diff --git a/src/pages/MyDataPage/MyDataPage.tsx b/src/pages/MyDataPage/MyDataPage.tsx index 20957ed7e..d7defc808 100644 --- a/src/pages/MyDataPage/MyDataPage.tsx +++ b/src/pages/MyDataPage/MyDataPage.tsx @@ -1,11 +1,7 @@ import { MyData } from '../../shared/canvas'; const MyDataPage = () => { - return ( - <div className="my-data-view view-container" style={{ padding: '0 1em' }}> - <MyData /> - </div> - ); + return <MyData />; }; export default MyDataPage; diff --git a/src/shared/canvas/MyData/MyData.tsx b/src/shared/canvas/MyData/MyData.tsx index ef4123a1a..5ca77d179 100644 --- a/src/shared/canvas/MyData/MyData.tsx +++ b/src/shared/canvas/MyData/MyData.tsx @@ -1,56 +1,16 @@ import * as React from 'react'; -import * as moment from 'moment'; import { useQuery } from 'react-query'; import { useSelector } from 'react-redux'; import { useNexusContext } from '@bbp/react-nexus'; import { notification } from 'antd'; -import { isObject, isString } from 'lodash'; +import { get, isObject, isString } from 'lodash'; import { MyDataHeader, MyDataTable } from '../../molecules'; import { RootState } from '../../store/reducers'; -import { TDateFilterType, TFilterOptions } from './types'; +import { TFilterOptions } from './types'; +import { makeDatetimePattern } from './utils'; import './styles.less'; -const makeDatetimePattern = ({ - dateFilterType, - singleDate, - dateStart, - dateEnd, -}: { - dateFilterType?: TDateFilterType; - singleDate?: string; - dateStart?: string; - dateEnd?: string; -}) => { - switch (dateFilterType) { - case 'after': { - if (!!singleDate && moment(singleDate).isValid()) { - return `${singleDate}..*`; - } - return undefined; - } - case 'before': { - if (!!singleDate && moment(singleDate).isValid()) { - return `*..${singleDate}`; - } - return undefined; - } - case 'range': { - if ( - !!dateStart && - !!dateEnd && - moment(dateStart).isValid() && - moment(dateEnd).isValid() && - moment(dateStart).isBefore(moment(dateEnd), 'days') - ) { - return `${dateStart}..${dateEnd}`; - } - return undefined; - } - default: - return undefined; - } -}; const HomeMyData: React.FC<{}> = () => { const nexus = useNexusContext(); const identities = useSelector( @@ -59,7 +19,6 @@ const HomeMyData: React.FC<{}> = () => { const issuerUri = identities?.find(item => item['@type'] === 'User')?.['@id']; const [ { - dataType, dateField, query, dateFilterType, @@ -71,6 +30,7 @@ const HomeMyData: React.FC<{}> = () => { sort, locate, issuer, + types, }, setFilterOptions, ] = React.useReducer( @@ -84,13 +44,13 @@ const HomeMyData: React.FC<{}> = () => { singleDate: undefined, dateStart: undefined, dateEnd: undefined, - dataType: [], query: '', offset: 0, size: 50, sort: ['-_createdAt', '@id'], locate: false, issuer: 'createdBy', + types: [], } ); @@ -116,6 +76,7 @@ const HomeMyData: React.FC<{}> = () => { ? `${dateField}-${dateFilterType}-${dateFilterRange}` : undefined; const order = sort.join('-'); + const resourceTypes = types?.map(item => get(item, 'value')); const { data: resources, isLoading } = useQuery({ queryKey: [ 'my-data-resources', @@ -127,6 +88,7 @@ const HomeMyData: React.FC<{}> = () => { issuer, date, order, + types: resourceTypes, }, ], retry: false, @@ -150,7 +112,8 @@ const HomeMyData: React.FC<{}> = () => { [dateField]: dateFilterRange, } : {}), - // type: dataType, + // @ts-ignore + type: resourceTypes, }), onError: error => { notification.error({ @@ -170,10 +133,10 @@ const HomeMyData: React.FC<{}> = () => { }); const total = resources?._total; return ( - <div className="home-mydata"> + <div className="my-data-view view-container"> <MyDataHeader {...{ - dataType, + types, dateField, query, dateFilterRange, diff --git a/src/shared/canvas/MyData/styles.less b/src/shared/canvas/MyData/styles.less index 5b58965aa..1e9186cac 100644 --- a/src/shared/canvas/MyData/styles.less +++ b/src/shared/canvas/MyData/styles.less @@ -1,3 +1,5 @@ -.home-mydata { - margin-top: 52px; +.my-data-view { + margin-top: 52px !important; + padding: 0 1em !important; + flex-direction: column !important; } From df2a518658ca4d47bfdd661c51153ce5b3355ae3 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Mon, 17 Jul 2023 11:03:23 +0200 Subject: [PATCH 158/192] f-3945/update: fix styles --- .../MyDataHeaderFilters/DateFieldSelector.tsx | 1 + .../MyDataHeaderFilters/IssuerSelector.tsx | 1 + .../MyDataHeaderFilters/TypeSelector.tsx | 10 +++------ src/shared/molecules/MyDataHeader/styles.less | 22 ++++++++++++++++--- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx index 408ae6591..9660a92cd 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx @@ -36,6 +36,7 @@ const DateFieldSelector = ({ ); return ( <Dropdown + className="date-field-selector" placement="bottomLeft" trigger={['click']} overlay={DateFieldMenu} diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx index 6675d8c5c..53016486e 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx @@ -12,6 +12,7 @@ const IssuerSelector = ({ issuer, setFilterOptions }: TIssuerSelectorProps) => { return ( <Radio.Group + className="issuer-selector" defaultValue={'createdBy'} value={issuer} onChange={onIssuerChange} diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx index 562d0c3c3..cb341d71a 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -107,7 +107,7 @@ const TypeSelector = ({ selectCallback, }); - const onSearchTypeChange = ({ + const onChangeTypeChange = ({ target: { value }, type, }: React.ChangeEvent<HTMLInputElement>) => { @@ -138,11 +138,7 @@ const TypeSelector = ({ e.preventDefault(); e.stopPropagation(); setFilterOptions({ - types: types?.find(item => item.key === type.key) - ? types.filter(item => item.key !== type.key) - : types - ? concat(types, type) - : [type], + types: [type], }); }; @@ -161,7 +157,7 @@ const TypeSelector = ({ className="my-data-type-filter-search" placeholder="Search for type" value={typeSearchValue} - onChange={onSearchTypeChange} + onChange={onChangeTypeChange} /> { <div className="count">{`${prettifyNumber( diff --git a/src/shared/molecules/MyDataHeader/styles.less b/src/shared/molecules/MyDataHeader/styles.less index 4c60abf39..17d103eea 100644 --- a/src/shared/molecules/MyDataHeader/styles.less +++ b/src/shared/molecules/MyDataHeader/styles.less @@ -35,13 +35,29 @@ &-actions { width: 100%; margin: 0; - display: grid; + display: flex; + flex-flow: row wrap; + // grid-template-columns: max-content max-content 300px 300px 1fr; gap: 15px; - grid-template-columns: max-content max-content 300px 300px 1fr; align-items: center; - justify-content: center; + justify-content: flex-start; align-content: center; } + .issuer-selector, + .date-field-selector { + width: 100%; + max-width: max-content; + } + .my-data-date-picker, + .my-data-type-picker { + width: 100%; + max-width: 300px; + } + .my-data-search-container { + flex: 0 1 25%; + width: 100%; + min-width: 250px; + } } .radio-filter { &.ant-radio-wrapper-checked { From 669935c08f7ad3b6c31647884e25c8f60e5adebc Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Mon, 17 Jul 2023 11:06:25 +0200 Subject: [PATCH 159/192] fix: use location from react-router instead of window --- .../DataExplorerGraphFlowNavigationStack/useNavigationStack.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts index 5046c5515..a03c45b2c 100644 --- a/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts +++ b/src/shared/organisms/DataExplorerGraphFlowNavigationStack/useNavigationStack.ts @@ -1,5 +1,5 @@ import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router'; +import { useHistory, useLocation } from 'react-router'; import { RootState } from '../../store/reducers'; import { DATA_EXPLORER_GRAPH_FLOW_DIGEST, @@ -13,6 +13,7 @@ import { const useNavigationStackManager = () => { const dispatch = useDispatch(); const history = useHistory(); + const location = useLocation(); const { rightNodes, leftNodes, referer } = useSelector( (state: RootState) => state.dataExplorer ); From 574199db06dacf4aa0acc3d8d81906b4518d2e14 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Mon, 17 Jul 2023 11:26:57 +0200 Subject: [PATCH 160/192] fix: anchor link icon in json editor seems not imported correctly --- src/shared/components/ResourceEditor/ResourceEditor.less | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 10cb04aeb..38b11c947 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -78,13 +78,10 @@ cursor: not-allowed !important; } - &.downloadable { - } - &::after { content: ''; display: inline-block; - background-image: url(../../images/AnchorLink.svg); + background-image: url(../../../shared/images/AnchorLink.svg); background-repeat: no-repeat; background-size: 14px 14px; width: 14px; From ebeab5c58cb4d21d8269cec81307348d3fa45ebf Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Thu, 13 Jul 2023 16:25:13 +0200 Subject: [PATCH 161/192] US 4038 // Reset predicate Signed-off-by: Dinika Saxena <dinikasaxenas@gmail.com> --- .../dataExplorer/DataExplorer.spec.tsx | 79 +++++-- src/subapps/dataExplorer/DataExplorer.tsx | 16 +- .../dataExplorer/PredicateSelector.tsx | 208 ++++++++++++------ src/subapps/dataExplorer/styles.less | 22 +- 4 files changed, 224 insertions(+), 101 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index fb125dd3f..051c22780 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -34,6 +34,11 @@ import { ALWAYS_DISPLAYED_COLUMNS, isNexusMetadata } from './DataExplorerUtils'; describe('DataExplorer', () => { const defaultTotalResults = 500_123; const mockResourcesOnPage1: Resource[] = getCompleteResources(); + const mockResourcesForPage2: Resource[] = [ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]; const server = setupServer( dataExplorerPageHandler(undefined, defaultTotalResults), @@ -203,10 +208,21 @@ describe('DataExplorer', () => { await expectRowCountToBe(3); }; - const openMenuFor = async (ariaLabel: string) => { - const menuInput = await screen.getByLabelText(ariaLabel, { + const getInputForLabel = async (label: string) => { + return (await screen.getByLabelText(label, { selector: 'input', - }); + })) as HTMLInputElement; + }; + + const getSelectedValueInMenu = async (menuLabel: string) => { + const input = await getInputForLabel(menuLabel); + return input + .closest('.ant-select-selector') + ?.querySelector('.ant-select-selection-item')?.innerHTML; + }; + + const openMenuFor = async (ariaLabel: string) => { + const menuInput = await getInputForLabel(ariaLabel); await userEvent.click(menuInput, { pointerEventsCheck: 0 }); await act(async () => { fireEvent.mouseDown(menuInput); @@ -217,7 +233,11 @@ describe('DataExplorer', () => { }; const selectPath = async (path: string) => { - selectOptionFromMenu(PathMenuLabel, path, CustomOptionSelector); + await selectOptionFromMenu(PathMenuLabel, path, CustomOptionSelector); + }; + + const selectPredicate = async (predicate: string) => { + await selectOptionFromMenu(PredicateMenuLabel, predicate); }; const selectOptionFromMenu = async ( @@ -226,7 +246,6 @@ describe('DataExplorer', () => { optionSelector?: string ) => { await openMenuFor(menuAriaLabel); - const allOptions = getVisibleOptionsFromMenu(); const option = await getDropdownOption(optionLabel, optionSelector); await userEvent.click(option, { pointerEventsCheck: 0 }); }; @@ -272,7 +291,9 @@ describe('DataExplorer', () => { return filteredCount; }; - const updateResourcesShownInTable = async (resources: Resource[]) => { + const updateResourcesShownInTable = async ( + resources: Resource[] = mockResourcesForPage2 + ) => { await expectRowCountToBe(10); await getRowsForNextPage(resources); await expectRowCountToBe(resources.length); @@ -285,6 +306,13 @@ describe('DataExplorer', () => { const showMetadataSwitch = async () => await screen.getByLabelText('Show metadata'); + const resetPredicate = async () => { + const resetPredicateButton = await screen.getByRole('button', { + name: /reset predicate/i, + }); + await userEvent.click(resetPredicateButton); + }; + it('shows columns for fields that are only in source data', async () => { await expectRowCountToBe(10); const column = await expectColumHeaderToExist('userProperty1'); @@ -582,7 +610,7 @@ describe('DataExplorer', () => { await selectPath('author'); await userEvent.click(container); await selectOptionFromMenu(PredicateMenuLabel, CONTAINS); - const valueInput = await screen.getByPlaceholderText('type the value...'); + const valueInput = await screen.getByPlaceholderText('Search for...'); await userEvent.type(valueInput, 'iggy'); await expectRowCountToBe(2); @@ -628,7 +656,7 @@ describe('DataExplorer', () => { await selectPath('author'); await userEvent.click(container); await selectOptionFromMenu(PredicateMenuLabel, DOES_NOT_CONTAIN); - const valueInput = await screen.getByPlaceholderText('type the value...'); + const valueInput = await screen.getByPlaceholderText('Search for...'); await userEvent.type(valueInput, 'iggy'); await expectRowCountToBe(2); @@ -714,14 +742,39 @@ describe('DataExplorer', () => { const originalColumns = getTotalColumns().length; - await selectOptionFromMenu( - PathMenuLabel, - metadataProperty, - CustomOptionSelector - ); + await selectPath(metadataProperty); await selectOptionFromMenu(PredicateMenuLabel, EXISTS); await expectColumHeaderToExist(metadataProperty); expect(getTotalColumns().length).toEqual(originalColumns + 1); + + await resetPredicate(); + expect(getTotalColumns().length).toEqual(originalColumns); + }); + + it('resets predicate fields when reset predicate clicked', async () => { + await updateResourcesShownInTable(mockResourcesForPage2); + + await selectPath('author'); + await selectPredicate(EXISTS); + + const selectedPathBefore = await getSelectedValueInMenu(PathMenuLabel); + expect(selectedPathBefore).toMatch(/author/); + + await expectRowCountToBe(2); + + await resetPredicate(); + + await expectRowCountToBe(3); + + const selectedPathAfter = await getSelectedValueInMenu(PathMenuLabel); + expect(selectedPathAfter).toBeFalsy(); + }); + + it('only shows predicate menu if path is selected', async () => { + await expectRowCountToBe(10); + expect(openMenuFor(PredicateMenuLabel)).rejects.toThrow(); + await selectPath('@type'); + expect(openMenuFor(PredicateMenuLabel)).resolves.not.toThrow(); }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 8d7f28bcf..71c5203f2 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -19,7 +19,7 @@ export interface DataExplorerConfiguration { offset: number; orgAndProject?: [string, string]; type: string | undefined; - predicateFilter: ((resource: Resource) => boolean) | null; + predicate: ((resource: Resource) => boolean) | null; selectedPath: string | null; } @@ -27,7 +27,7 @@ export const DataExplorer: React.FC<{}> = () => { const [showMetadataColumns, setShowMetadataColumns] = useState(false); const [ - { pageSize, offset, orgAndProject, predicateFilter, type, selectedPath }, + { pageSize, offset, orgAndProject, predicate, type, selectedPath }, updateTableConfiguration, ] = useReducer( ( @@ -39,7 +39,7 @@ export const DataExplorer: React.FC<{}> = () => { offset: 0, orgAndProject: undefined, type: undefined, - predicateFilter: null, + predicate: null, selectedPath: null, } ); @@ -53,10 +53,8 @@ export const DataExplorer: React.FC<{}> = () => { const currentPageDataSource: Resource[] = resources?._results || []; - const displayedDataSource = predicateFilter - ? currentPageDataSource.filter(resource => { - return predicateFilter(resource); - }) + const displayedDataSource = predicate + ? currentPageDataSource.filter(predicate) : currentPageDataSource; return ( @@ -90,9 +88,7 @@ export const DataExplorer: React.FC<{}> = () => { <DatasetCount nexusTotal={resources?._total ?? 0} totalOnPage={resources?._results?.length ?? 0} - totalFiltered={ - predicateFilter ? displayedDataSource.length : undefined - } + totalFiltered={predicate ? displayedDataSource.length : undefined} /> <div className="data-explorer-toggles"> <Switch diff --git a/src/subapps/dataExplorer/PredicateSelector.tsx b/src/subapps/dataExplorer/PredicateSelector.tsx index 833d5c3ce..6208c688c 100644 --- a/src/subapps/dataExplorer/PredicateSelector.tsx +++ b/src/subapps/dataExplorer/PredicateSelector.tsx @@ -1,20 +1,18 @@ -import { Input, Select } from 'antd'; -import React, { useState } from 'react'; -import { DataExplorerConfiguration } from './DataExplorer'; -import './styles.less'; +import { UndoOutlined } from '@ant-design/icons'; import { Resource } from '@bbp/nexus-sdk'; -import { normalizeString } from '../../utils/stringUtils'; -import { clsx } from 'clsx'; -import { BaseOptionType } from 'antd/lib/select'; +import { Button, Form, Input, Select } from 'antd'; +import { FormInstance } from 'antd/es/form'; import { DefaultOptionType } from 'antd/lib/cascader'; +import React, { useMemo, useRef } from 'react'; +import { normalizeString } from '../../utils/stringUtils'; +import { DataExplorerConfiguration } from './DataExplorer'; import { - ALWAYS_DISPLAYED_COLUMNS, columnFromPath, isObject, isUserColumn, sortColumns, } from './DataExplorerUtils'; -import { ClearOutlined } from '@ant-design/icons'; +import './styles.less'; interface Props { dataSource: Resource[]; @@ -25,41 +23,40 @@ export const PredicateSelector: React.FC<Props> = ({ dataSource, onPredicateChange, }: Props) => { - const [path, setPath] = useState<string>(DEFAULT_OPTION); - - const [predicate, setPredicate] = useState<PredicateFilterOptions['value']>( - DEFAULT_OPTION - ); - const [searchTerm, setSearchTerm] = useState<string | null>(null); + const formRef = useRef<FormInstance>(null); const predicateFilterOptions: PredicateFilterOptions[] = [ - { value: DEFAULT_OPTION }, { value: EXISTS }, { value: DOES_NOT_EXIST }, { value: CONTAINS }, { value: DOES_NOT_CONTAIN }, ]; + const allPathOptions = useMemo( + () => pathOptions([...getAllPaths(dataSource)]), + [dataSource] + ); + const predicateSelected = ( path: string, - predicate: PredicateFilterOptions['value'], + predicate: PredicateFilterOptions['value'] | null, searchTerm: string | null ) => { - if (path === DEFAULT_OPTION || predicate === DEFAULT_OPTION) { - onPredicateChange({ predicateFilter: null, selectedPath: null }); + if (!path || !predicate) { + onPredicateChange({ predicate: null, selectedPath: null }); } switch (predicate) { case EXISTS: onPredicateChange({ - predicateFilter: (resource: Resource) => + predicate: (resource: Resource) => checkPathExistence(resource, path, 'exists'), selectedPath: path, }); break; case DOES_NOT_EXIST: onPredicateChange({ - predicateFilter: (resource: Resource) => + predicate: (resource: Resource) => checkPathExistence(resource, path, 'does-not-exist'), selectedPath: path, }); @@ -67,18 +64,18 @@ export const PredicateSelector: React.FC<Props> = ({ case CONTAINS: if (searchTerm) { onPredicateChange({ - predicateFilter: (resource: Resource) => + predicate: (resource: Resource) => doesResourceContain(resource, path, searchTerm, 'contains'), selectedPath: path, }); } else { - onPredicateChange({ predicateFilter: null, selectedPath: null }); + onPredicateChange({ predicate: null, selectedPath: null }); } break; case DOES_NOT_CONTAIN: if (searchTerm) { onPredicateChange({ - predicateFilter: (resource: Resource) => + predicate: (resource: Resource) => doesResourceContain( resource, path, @@ -88,77 +85,142 @@ export const PredicateSelector: React.FC<Props> = ({ selectedPath: path, }); } else { - onPredicateChange({ predicateFilter: null, selectedPath: null }); + onPredicateChange({ predicate: null, selectedPath: null }); } break; default: - onPredicateChange({ predicateFilter: null, selectedPath: null }); + onPredicateChange({ predicate: null, selectedPath: null }); } }; + const getFormFieldValue = (fieldName: string) => { + return formRef.current?.getFieldValue(fieldName) ?? ''; + }; + + const setFormField = (fieldName: string, fieldValue: string) => { + if (formRef.current) { + formRef.current.setFieldValue(fieldName, fieldValue); + } + }; + + const onReset = () => { + const form = formRef.current; + if (form) { + form.resetFields(); + } + + onPredicateChange({ predicate: null, selectedPath: null }); + }; + const shouldShowValueInput = - predicate === CONTAINS || predicate === DOES_NOT_CONTAIN; + getFormFieldValue(PREDICATE_FIELD) === CONTAINS || + getFormFieldValue(PREDICATE_FIELD) === DOES_NOT_CONTAIN; return ( - <div className="form-container"> + <Form ref={formRef} name="predicate-selection" className="form-container"> <span className="label">with </span> - <Select - options={pathOptions([...getAllPaths(dataSource)])} - onSelect={pathLabel => { - setPath(pathLabel); - predicateSelected(pathLabel, predicate, searchTerm); - }} - optionLabelProp="label" - aria-label="path-selector" - style={{ width: 200 }} - dropdownMatchSelectWidth={false} - className="select-menu" - popupClassName="search-menu" - /> - - <span className="label">= </span> - - <Select - options={predicateFilterOptions} - onSelect={(predicateLabel: PredicateFilterOptions['value']) => { - setPredicate(predicateLabel); - predicateSelected(path, predicateLabel, searchTerm); - }} - aria-label="predicate-selector" - className={clsx('select-menu', shouldShowValueInput && 'greyed-out')} - popupClassName="search-menu" - allowClear={true} - onClear={() => { - setPredicate(DEFAULT_OPTION); - predicateSelected(path, DEFAULT_OPTION, searchTerm); - }} - /> - - {shouldShowValueInput && ( - <Input - placeholder="type the value..." - aria-label="predicate-value-input" - bordered={false} - className="predicate-value-input" - allowClear={false} - onChange={event => { - setSearchTerm(event.target.value); - predicateSelected(path, predicate, event.target.value); + <Form.Item name="path" noStyle> + <Select + options={allPathOptions} + showSearch={true} + onSelect={pathLabel => { + setFormField(PATH_FIELD, pathLabel); + predicateSelected( + pathLabel, + getFormFieldValue(PREDICATE_FIELD), + getFormFieldValue(SEARCH_TERM_FIELD) + ); }} + allowClear={true} + onClear={() => { + onReset(); + }} + virtual={true} + className="select-menu" + popupClassName="search-menu" + optionLabelProp="label" + aria-label="path-selector" + style={{ width: 200, minWidth: 'max-content' }} + dropdownMatchSelectWidth={false} // This ensures that the items in the dropdown list are always fully legible (ie they are not truncated) just because the input of select is too short. /> + </Form.Item> + + {getFormFieldValue(PATH_FIELD) && ( + <> + <span className="label">= </span> + + <Form.Item name="predicate" noStyle> + <Select + options={predicateFilterOptions} + onSelect={(predicateLabel: PredicateFilterOptions['value']) => { + setFormField(PREDICATE_FIELD, predicateLabel); + predicateSelected( + getFormFieldValue(PATH_FIELD), + predicateLabel, + getFormFieldValue(SEARCH_TERM_FIELD) + ); + }} + aria-label="predicate-selector" + className="select-menu reduced-width" + popupClassName="search-menu" + autoFocus={true} + allowClear={true} + onClear={() => { + predicateSelected( + getFormFieldValue(PATH_FIELD), + null, + getFormFieldValue(SEARCH_TERM_FIELD) + ); + }} + /> + </Form.Item> + </> + )} + + {shouldShowValueInput && ( + <Form.Item name="searchTerm" noStyle> + <Input + placeholder="Search for..." + aria-label="predicate-value-input" + className="predicate-value-input" + allowClear={false} + autoFocus={true} + onChange={event => { + const term = event.target.value; + setFormField(SEARCH_TERM_FIELD, term); + predicateSelected( + getFormFieldValue(PATH_FIELD), + getFormFieldValue(PREDICATE_FIELD), + term + ); + }} + /> + </Form.Item> )} - </div> + + <Button + onClick={onReset} + disabled={!getFormFieldValue(PATH_FIELD)} + type="text" + className="text-button" + > + Reset predicate <UndoOutlined /> + </Button> + </Form> ); }; -export const DEFAULT_OPTION = '-'; export const DOES_NOT_EXIST = 'Does not exist'; export const EXISTS = 'Exists'; export const CONTAINS = 'Contains'; export const DOES_NOT_CONTAIN = 'Does not contain'; +const PATH_FIELD = 'path'; +const PREDICATE_FIELD = 'predicate'; +const SEARCH_TERM_FIELD = 'searchTerm'; + export type PredicateFilterT = | typeof DOES_NOT_EXIST | typeof EXISTS @@ -167,7 +229,7 @@ export type PredicateFilterT = | null; type PredicateFilterOptions = { - value: Exclude<PredicateFilterT, null> | typeof DEFAULT_OPTION; + value: Exclude<PredicateFilterT, null>; }; // Creates <Option /> element for each path. Also adds a class of "first-metadata-path" for the first path generated for a metadata column. diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 23966587b..3445325a3 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -53,6 +53,10 @@ } } + .text-button { + color: @fusion-gray-7; + } + .form-container { display: flex; align-items: center; @@ -60,6 +64,10 @@ max-width: fit-content; margin: 0 10px; + .ant-form-item { + margin-bottom: 0; + } + .label { font-size: 12px; font-weight: 300; @@ -129,6 +137,12 @@ font-weight: 700; } } + .select-menu.reduced-width { + .ant-select-selector { + min-width: 140px; + width: max-content; + } + } } .select-menu.greyed-out { @@ -161,11 +175,9 @@ } .predicate-value-input { - border-bottom: 1px solid @medium-gray; color: @fusion-blue-8; - &::placeholder { - color: @fusion-blue-8; - } + background: @fusion-main-bg; + width: 200px; } } @@ -175,7 +187,7 @@ min-width: 90vw; } .ant-table { - background: #f5f5f5; + background: @fusion-main-bg; } .ant-table-thead > tr > th.data-explorer-column { background-color: #f5f5f5 !important; From a77e73a3003db13ab5ea727375f58d40a845505f Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Thu, 13 Jul 2023 19:09:30 +0200 Subject: [PATCH 162/192] 4042 // Enable sorting of columns --- .../dataExplorer/DataExplorer.spec.tsx | 39 ++++++++++++++++++- .../dataExplorer/DataExplorerTable.tsx | 6 ++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 051c22780..90bbd3c77 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -109,13 +109,18 @@ describe('DataExplorer', () => { const expectColumHeaderToExist = async (name: string) => { const nameReg = new RegExp(getColumnTitle(name), 'i'); const header = await screen.getByText(nameReg, { - selector: 'th', + selector: 'th .ant-table-column-title', exact: false, }); expect(header).toBeInTheDocument(); return header; }; + const getColumnSorter = async (colName: string) => { + const column = await expectColumHeaderToExist(colName); + return column.closest('.ant-table-column-sorters'); + }; + const getTotalColumns = () => { return Array.from(container.querySelectorAll('th')); }; @@ -173,6 +178,11 @@ describe('DataExplorer', () => { return typeColumn?.textContent; }; + const columnTextFromRow = (row: Element, colName: string) => { + const column = row.querySelector(`td.data-explorer-column-${colName}`); + return column?.textContent; + }; + const visibleTableRows = () => { return container.querySelectorAll('table tbody tr.data-explorer-row'); }; @@ -313,6 +323,17 @@ describe('DataExplorer', () => { await userEvent.click(resetPredicateButton); }; + const expectRowsInOrder = async (expectedOrder: Resource[]) => { + for await (const [index, row] of visibleTableRows().entries()) { + const text = await columnTextFromRow(row, 'author'); + if (expectedOrder[index].author) { + expect(text).toMatch(JSON.stringify(expectedOrder[index].author)); + } else { + expect(text).toMatch(/No data/i); + } + } + }; + it('shows columns for fields that are only in source data', async () => { await expectRowCountToBe(10); const column = await expectColumHeaderToExist('userProperty1'); @@ -777,4 +798,20 @@ describe('DataExplorer', () => { await selectPath('@type'); expect(openMenuFor(PredicateMenuLabel)).resolves.not.toThrow(); }); + + it('sorts table columns', async () => { + const dataSource = [ + getMockResource('self1', { author: 'tweaty', edition: 1 }), + getMockResource('self2', { edition: 2001 }), + getMockResource('self3', { year: 2013, author: 'piggy' }), + ]; + await updateResourcesShownInTable(dataSource); + + await expectRowsInOrder(dataSource); + + const authorColumnSorter = await getColumnSorter('author'); + await userEvent.click(authorColumnSorter!); + + await expectRowsInOrder([dataSource[1], dataSource[2], dataSource[0]]); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index 51230536d..54762864a 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -115,7 +115,11 @@ const defaultColumnConfig = (colName: string): ColumnType<Resource> => { title: getColumnTitle(colName), dataIndex: colName, className: `data-explorer-column data-explorer-column-${colName}`, - sorter: false, + sorter: (a, b) => { + return JSON.stringify(a[colName] ?? '').localeCompare( + JSON.stringify(b[colName] ?? '') + ); + }, render: text => { if (text === undefined) { // Text will also be undefined if a certain resource does not have `colName` as its property From cf6c9274858aac8ed2e9b2b7df7a4de9d3bccc1c Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Fri, 14 Jul 2023 13:13:43 +0200 Subject: [PATCH 163/192] 4039 // Toggle if empty cells should be highlighted or not Signed-off-by: Dinika Saxena <dinikasaxenas@gmail.com> --- .../dataExplorer/DataExplorer.spec.tsx | 24 +++++++++++++++ src/subapps/dataExplorer/DataExplorer.tsx | 29 ++++++++++++++---- .../dataExplorer/DataExplorerTable.tsx | 30 ++++++++++++------- .../dataExplorer/DataExplorerUtils.tsx | 1 + src/subapps/dataExplorer/styles.less | 3 +- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 90bbd3c77..4c7d09950 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -316,6 +316,9 @@ describe('DataExplorer', () => { const showMetadataSwitch = async () => await screen.getByLabelText('Show metadata'); + const showEmptyDataCellsSwitch = async () => + await screen.getByLabelText('Show empty data cells'); + const resetPredicate = async () => { const resetPredicateButton = await screen.getByRole('button', { name: /reset predicate/i, @@ -814,4 +817,25 @@ describe('DataExplorer', () => { await expectRowsInOrder([dataSource[1], dataSource[2], dataSource[0]]); }); + + it('does not show "No data" cell if "Show empty data cells" toggle is turned off', async () => { + await expectRowCountToBe(10); + const resourceWithMissingProperty = mockResourcesOnPage1.find( + res => !('specialProperty' in res) + )!; + const textForSpecialProperty = await getTextForColumn( + resourceWithMissingProperty, + 'specialProperty' + ); + expect(textForSpecialProperty).toMatch(/No data/i); + + const button = await showEmptyDataCellsSwitch(); + await userEvent.click(button); + + const textForSpecialPropertyAfter = await getTextForColumn( + resourceWithMissingProperty, + 'specialProperty' + ); + expect(textForSpecialPropertyAfter).toMatch(''); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 71c5203f2..66a692e3a 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -1,6 +1,6 @@ import { Resource } from '@bbp/nexus-sdk'; import { Switch } from 'antd'; -import React, { useReducer, useState } from 'react'; +import React, { useMemo, useReducer, useState } from 'react'; import { DataExplorerTable } from './DataExplorerTable'; import { columnFromPath, @@ -25,6 +25,7 @@ export interface DataExplorerConfiguration { export const DataExplorer: React.FC<{}> = () => { const [showMetadataColumns, setShowMetadataColumns] = useState(false); + const [showEmptyDataCells, setShowEmptyDataCells] = useState(true); const [ { pageSize, offset, orgAndProject, predicate, type, selectedPath }, @@ -57,6 +58,16 @@ export const DataExplorer: React.FC<{}> = () => { ? currentPageDataSource.filter(predicate) : currentPageDataSource; + const memoizedColumns = useMemo( + () => + columnsFromDataSource( + currentPageDataSource, + showMetadataColumns, + selectedPath + ), + [currentPageDataSource, showMetadataColumns, selectedPath] + ); + return ( <div className="data-explorer-contents"> <div className="data-explorer-filters"> @@ -99,6 +110,15 @@ export const DataExplorer: React.FC<{}> = () => { className="data-explorer-toggle" /> <label htmlFor="show-metadata-columns">Show metadata</label> + + <Switch + defaultChecked={true} + checked={showEmptyDataCells} + onClick={isChecked => setShowEmptyDataCells(isChecked)} + id="show-empty-data-cells" + className="data-explorer-toggle" + /> + <label htmlFor="show-empty-data-cells">Show empty data cells</label> </div> </div> )} @@ -106,15 +126,12 @@ export const DataExplorer: React.FC<{}> = () => { <DataExplorerTable isLoading={isLoading} dataSource={displayedDataSource} - columns={columnsFromDataSource( - currentPageDataSource, - showMetadataColumns, - selectedPath - )} + columns={memoizedColumns} total={resources?._total} pageSize={pageSize} offset={offset} updateTableConfiguration={updateTableConfiguration} + showEmptyDataCells={showEmptyDataCells} /> </div> ); diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index 54762864a..c3c0ca9ac 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -19,6 +19,7 @@ interface TDataExplorerTable { offset: number; updateTableConfiguration: React.Dispatch<Partial<DataExplorerConfiguration>>; columns: string[]; + showEmptyDataCells: boolean; } type TColumnNameToConfig = Map<string, ColumnType<Resource>>; @@ -31,6 +32,7 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ pageSize, offset, updateTableConfiguration, + showEmptyDataCells, }: TDataExplorerTable) => { const history = useHistory(); const location = useLocation(); @@ -65,7 +67,7 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ return ( <Table<Resource> - columns={columnsConfig(columns)} + columns={columnsConfig(columns, showEmptyDataCells)} dataSource={dataSource} rowKey={record => record._self} onRow={resource => ({ @@ -91,15 +93,18 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ * 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). */ export const columnsConfig = ( - columnNames: string[] + columnNames: string[], + showEmptyDataCells: boolean ): ColumnType<Resource>[] => { const colNameToConfig = new Map( - columnNames.length === 0 ? [] : initialTableConfig() + columnNames.length === 0 ? [] : initialTableConfig(showEmptyDataCells) ); for (const columnName of columnNames) { if (!colNameToConfig.has(columnName)) { - colNameToConfig.set(columnName, { ...defaultColumnConfig(columnName) }); + colNameToConfig.set(columnName, { + ...defaultColumnConfig(columnName, showEmptyDataCells), + }); } } @@ -109,7 +114,10 @@ export const columnsConfig = ( export const getColumnTitle = (colName: string) => startCase(colName).toUpperCase(); -const defaultColumnConfig = (colName: string): ColumnType<Resource> => { +const defaultColumnConfig = ( + colName: string, + showEmptyDataCells: boolean +): ColumnType<Resource> => { return { key: colName, title: getColumnTitle(colName), @@ -121,7 +129,7 @@ const defaultColumnConfig = (colName: string): ColumnType<Resource> => { ); }, render: text => { - if (text === undefined) { + if (text === undefined && showEmptyDataCells) { // Text will also be undefined if a certain resource does not have `colName` as its property return <NoDataCell />; } @@ -130,24 +138,24 @@ const defaultColumnConfig = (colName: string): ColumnType<Resource> => { }; }; -const initialTableConfig = () => { +const initialTableConfig = (showEmptyDataCells: boolean) => { const colNameToConfig: TColumnNameToConfig = new Map(); const projectKey = '_project'; const typeKey = '@type'; const projectConfig: ColumnType<Resource> = { - ...defaultColumnConfig(projectKey), + ...defaultColumnConfig(projectKey, showEmptyDataCells), title: 'PROJECT', render: text => { if (text) { const { org, project } = makeOrgProjectTuple(text); return `${org}/${project}`; } - return <NoDataCell />; + return showEmptyDataCells && <NoDataCell />; }, }; const typeConfig: ColumnType<Resource> = { - ...defaultColumnConfig(typeKey), + ...defaultColumnConfig(typeKey, showEmptyDataCells), title: 'TYPE', render: text => { let types = ''; @@ -167,7 +175,7 @@ const initialTableConfig = () => { <div style={{ whiteSpace: 'pre-wrap' }}>{types}</div> </Tooltip> ) : ( - <NoDataCell /> + showEmptyDataCells && <NoDataCell /> ); }, }; diff --git a/src/subapps/dataExplorer/DataExplorerUtils.tsx b/src/subapps/dataExplorer/DataExplorerUtils.tsx index e22cc9748..73fe2562f 100644 --- a/src/subapps/dataExplorer/DataExplorerUtils.tsx +++ b/src/subapps/dataExplorer/DataExplorerUtils.tsx @@ -104,6 +104,7 @@ export const useAggregations = ( onError: error => { notification.error({ message: 'Aggregations could not be fetched' }); }, + staleTime: Infinity, }); }; diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 3445325a3..4dd17543b 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -17,12 +17,12 @@ max-width: 90vw; justify-content: space-between; align-items: center; + margin-bottom: 20px; } .data-explorer-count { color: @fusion-neutral-7; margin-left: 20px; - margin-bottom: 28px; span { margin-right: 24px; @@ -37,6 +37,7 @@ .data-explorer-toggle { border: 1px solid @fusion-blue-8; box-sizing: content-box; + margin-left: 30px; &[aria-checked='true'] { background-color: @fusion-blue-8; From a337b6462e5f6f2f9c5520a8ae173c92e27bef7a Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Fri, 14 Jul 2023 15:12:27 +0200 Subject: [PATCH 164/192] 4040 // Make table header always visible Signed-off-by: Dinika Saxena <dinikasaxenas@gmail.com> --- src/subapps/dataExplorer/DataExplorerTable.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index c3c0ca9ac..c2141c4e7 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -85,6 +85,7 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ }, }} pagination={tablePaginationConfig} + sticky={{ offsetHeader: 52 }} /> ); }; @@ -153,6 +154,12 @@ const initialTableConfig = (showEmptyDataCells: boolean) => { } return showEmptyDataCells && <NoDataCell />; }, + sorter: (a, b) => { + const tupleA = makeOrgProjectTuple(a[projectKey] ?? ''); + const tupleB = makeOrgProjectTuple(b[projectKey] ?? ''); + + return (tupleA.project ?? '').localeCompare(tupleB.project); + }, }; const typeConfig: ColumnType<Resource> = { ...defaultColumnConfig(typeKey, showEmptyDataCells), From c0878adf2057cd1294973c7c63df4c216fa4e0bc Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Mon, 17 Jul 2023 16:56:02 +0200 Subject: [PATCH 165/192] fix: require the image in js and pass it to the css as var --- src/shared/components/ResourceEditor/ResourceEditor.less | 2 +- src/shared/components/ResourceEditor/index.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 38b11c947..69f041ec0 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -81,7 +81,7 @@ &::after { content: ''; display: inline-block; - background-image: url(../../../shared/images/AnchorLink.svg); + background-image: var(--resource-link-anchor-icon); background-repeat: no-repeat; background-size: 14px 14px; width: 14px; diff --git a/src/shared/components/ResourceEditor/index.tsx b/src/shared/components/ResourceEditor/index.tsx index 2d230fbee..e9f0facec 100644 --- a/src/shared/components/ResourceEditor/index.tsx +++ b/src/shared/components/ResourceEditor/index.tsx @@ -29,9 +29,9 @@ import { } from './useEditorTooltip'; import { DATA_EXPLORER_GRAPH_FLOW_PATH } from '../../store/reducers/data-explorer'; import ResourceResolutionCache from './ResourcesLRUCache'; - import './ResourceEditor.less'; +const AnchorLinkIcon = require('../../images/AnchorLink.svg'); export interface ResourceEditorProps { rawData: { [key: string]: any }; onSubmit: (rawData: { [key: string]: any }) => void; @@ -204,6 +204,11 @@ const ResourceEditor: React.FunctionComponent<ResourceEditorProps> = props => { <div data-testid="resource-editor" className={valid ? 'resource-editor' : 'resource-editor _invalid'} + style={ + { + '--resource-link-anchor-icon': `url(${AnchorLinkIcon})`, + } as React.CSSProperties + } > {showControlPanel && ( <div className="control-panel"> From de471cbf059c3dda1c82c3b7e09f6deee2d5c3fb Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Mon, 17 Jul 2023 21:18:53 +0200 Subject: [PATCH 166/192] fix: resource type might be a string or array --- .../containers/ResourceViewActionsContainer.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/shared/containers/ResourceViewActionsContainer.tsx b/src/shared/containers/ResourceViewActionsContainer.tsx index fa7b69667..52ba203f2 100644 --- a/src/shared/containers/ResourceViewActionsContainer.tsx +++ b/src/shared/containers/ResourceViewActionsContainer.tsx @@ -5,7 +5,7 @@ import { useNexusContext } from '@bbp/react-nexus'; import { Button, Col, Dropdown, Menu, Row, notification } from 'antd'; import { generatePath, Link, useHistory, useLocation } from 'react-router-dom'; import { useSelector } from 'react-redux'; -import { uniq } from 'lodash'; +import { isArray, isString, uniq } from 'lodash'; import { makeResourceUri } from '../utils'; import { RootState } from '../store/reducers'; import { useOrganisationsSubappContext } from '../../subapps/admin'; @@ -123,10 +123,15 @@ const ResourceViewActionsContainer: React.FC<{ ); nexus.Resource.get(orgLabel, projectLabel, encodedResourceId).then( resource => { - // @ts-ignore - if (resource && resource['@type'].includes('View')) { - // @ts-ignore - setView(resource); + const resourceType = resource ? (resource as Resource)['@type'] : null; + const isView = + resourceType && isArray(resourceType) + ? resourceType.includes('View') + : isString(resourceType) + ? resourceType === 'View' + : false; + if (isView) { + setView(resource as Resource); } } ); From ba8e169c6abb569b7ea7ffeb6422d40cd5758615 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 09:35:26 +0200 Subject: [PATCH 167/192] f-3945/fix: the type selection/deslection --- .../MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx | 2 +- src/shared/molecules/MyDataTable/MyDataTable.tsx | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx index cb341d71a..b1a7f3263 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -138,7 +138,7 @@ const TypeSelector = ({ e.preventDefault(); e.stopPropagation(); setFilterOptions({ - types: [type], + types: types?.find(item => item.value === type.value) ? [] : [type], }); }; diff --git a/src/shared/molecules/MyDataTable/MyDataTable.tsx b/src/shared/molecules/MyDataTable/MyDataTable.tsx index d73b27a15..82cf6a1dd 100644 --- a/src/shared/molecules/MyDataTable/MyDataTable.tsx +++ b/src/shared/molecules/MyDataTable/MyDataTable.tsx @@ -4,6 +4,7 @@ import React, { useReducer, useEffect, useState, + CSSProperties, } from 'react'; import { Button, Empty, Table, Tag, Tooltip, notification } from 'antd'; import { @@ -155,7 +156,7 @@ type TSorterProps = { onSortDescend(): void; onSortAscend(): void; }; - +const columnWhiteSpaceWrap = { whiteSpace: 'nowrap' } as CSSProperties; const Sorter = ({ onSortDescend, onSortAscend, order, name }: TSorterProps) => { if (!order) { return ( @@ -239,7 +240,7 @@ const MyDataTable: React.FC<TProps> = ({ return ( <Tooltip title={resourceId}> <Button - style={{ padding: 0 }} + style={{ padding: 0, ...columnWhiteSpaceWrap }} type="link" onClick={() => goToResource(org, project, resourceId)} > @@ -262,7 +263,7 @@ const MyDataTable: React.FC<TProps> = ({ : 'asc' : undefined; return ( - <div> + <div style={columnWhiteSpaceWrap}> organization / project {(!query || query.trim() === '') && ( <Sorter @@ -328,7 +329,7 @@ const MyDataTable: React.FC<TProps> = ({ : 'asc' : undefined; return ( - <div> + <div style={columnWhiteSpaceWrap}> updated date {(!query || query.trim() === '') && ( <Sorter @@ -356,7 +357,7 @@ const MyDataTable: React.FC<TProps> = ({ : 'asc' : undefined; return ( - <div> + <div style={columnWhiteSpaceWrap}> created date {(!query || query.trim() === '') && ( <Sorter From 05cb70cc22b12ad79e37b74825c43ec137ad8403 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 09:41:13 +0200 Subject: [PATCH 168/192] fix: replace the icon with highlight --- .../ResourceEditor/ResourceEditor.less | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index a4da533ae..c54222f47 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -65,6 +65,10 @@ .fusion-resource-link { color: #0974ca !important; cursor: pointer !important; + background-color: rgba(#0974ca, 0.12); + border-radius: 4px; + padding: 1px; + border: 0.5px solid rgba(#0974ca, 0.14); &.wait-for-tooltip { cursor: progress !important; @@ -75,18 +79,17 @@ &.error { cursor: not-allowed !important; } - &.downloadable { - } - &::after { - content: ''; - display: inline-block; - background-image: url(../../images/AnchorLink.svg); - background-repeat: no-repeat; - background-size: 14px 14px; - width: 14px; - height: 14px; - margin-left: 10px; - } + + // &::after { + // content: ''; + // display: inline-block; + // background-image: url(../../images/AnchorLink.svg); + // background-repeat: no-repeat; + // background-size: 14px 14px; + // width: 14px; + // height: 14px; + // margin-left: 10px; + // } } .CodeMirror-lines { From b536113372de1e1e328ef1dd04080818eb205405 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 11:58:14 +0200 Subject: [PATCH 169/192] f-3945/update: change the design --- src/shared/canvas/MyData/types.ts | 9 +- .../molecules/MyDataHeader/MyDataHeader.tsx | 30 +-- .../MyDataHeaderFilters/IssuerSelector.tsx | 9 +- .../MyDataHeaderFilters/PageTitle.tsx | 13 ++ .../MyDataHeaderFilters/SearchInput.tsx | 8 +- .../MyDataHeaderFilters/TypeSelector.tsx | 2 +- .../MyDataHeaderFilters/index.tsx | 43 +++- src/shared/molecules/MyDataHeader/styles.less | 209 ++++++++++++------ 8 files changed, 220 insertions(+), 103 deletions(-) create mode 100644 src/shared/molecules/MyDataHeader/MyDataHeaderFilters/PageTitle.tsx diff --git a/src/shared/canvas/MyData/types.ts b/src/shared/canvas/MyData/types.ts index 2c0ebf5df..1d118c3ca 100644 --- a/src/shared/canvas/MyData/types.ts +++ b/src/shared/canvas/MyData/types.ts @@ -31,7 +31,14 @@ export type TTitleProps = { label: string; total?: number; }; -export type THeaderFilterProps = Omit<THeaderProps, 'total' | 'sort'>; +export type THeaderFilterProps = Pick< + THeaderProps, + 'types' | 'dateField' | 'setFilterOptions' +>; +export type THeaderTitleProps = Pick< + THeaderProps, + 'total' | 'query' | 'locate' | 'issuer' | 'setFilterOptions' +>; export type THandleMenuSelect = MenuProps['onClick']; export type TTypeDateItem = { key: string; diff --git a/src/shared/molecules/MyDataHeader/MyDataHeader.tsx b/src/shared/molecules/MyDataHeader/MyDataHeader.tsx index 9eeea6b3f..a44bb5086 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeader.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeader.tsx @@ -1,19 +1,8 @@ import * as React from 'react'; -import * as pluralize from 'pluralize'; -import { THeaderProps, TTitleProps } from '../../../shared/canvas/MyData/types'; -import MyDataHeaderFilters from './MyDataHeaderFilters'; -import { prettifyNumber } from '../../../utils/formatNumber'; +import { THeaderProps } from '../../../shared/canvas/MyData/types'; +import { MyDataHeaderTitle, MyDataHeaderFilters } from './MyDataHeaderFilters'; import './styles.less'; -const Title = ({ text, label, total }: TTitleProps) => { - return ( - <div className="my-data-table-header-title"> - <span>{text}</span> - <span>{total ? `${prettifyNumber(total)} ${label}` : ''}</span> - </div> - ); -}; - const MyDataHeader: React.FC<THeaderProps> = ({ total, types, @@ -24,12 +13,17 @@ const MyDataHeader: React.FC<THeaderProps> = ({ issuer, }) => { return ( - <div className="my-data-table-header"> - <Title - text="My data" - label={pluralize('Dataset', Number(total))} - total={total} + <div className="my-data-header"> + <MyDataHeaderTitle + {...{ + total, + query, + setFilterOptions, + locate, + issuer, + }} /> + <div className="divider" /> <MyDataHeaderFilters {...{ types, diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx index 53016486e..6a6b0fd39 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx @@ -1,10 +1,7 @@ import { Radio, RadioChangeEvent } from 'antd'; -import { THeaderFilterProps } from 'shared/canvas/MyData/types'; +import { THeaderProps } from 'shared/canvas/MyData/types'; -type TIssuerSelectorProps = Pick< - THeaderFilterProps, - 'issuer' | 'setFilterOptions' ->; +type TIssuerSelectorProps = Pick<THeaderProps, 'issuer' | 'setFilterOptions'>; const IssuerSelector = ({ issuer, setFilterOptions }: TIssuerSelectorProps) => { const onIssuerChange = (e: RadioChangeEvent) => @@ -12,7 +9,7 @@ const IssuerSelector = ({ issuer, setFilterOptions }: TIssuerSelectorProps) => { return ( <Radio.Group - className="issuer-selector" + className="my-data-header-title_issuer_selector" defaultValue={'createdBy'} value={issuer} onChange={onIssuerChange} diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/PageTitle.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/PageTitle.tsx new file mode 100644 index 000000000..8f9ae7015 --- /dev/null +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/PageTitle.tsx @@ -0,0 +1,13 @@ +import { TTitleProps } from 'shared/canvas/MyData/types'; +import { prettifyNumber } from '../../../../utils/formatNumber'; + +const PageTitle = ({ text, label, total }: TTitleProps) => { + return ( + <div className="my-data-header-title_heading"> + <span>{text}</span> + <span>{total ? `${prettifyNumber(total)} ${label}` : ''}</span> + </div> + ); +}; + +export default PageTitle; diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx index f5de2a75d..71cb13378 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx @@ -1,9 +1,9 @@ import { Checkbox, Input } from 'antd'; import { CheckboxChangeEvent } from 'antd/lib/checkbox'; -import { THeaderFilterProps } from 'shared/canvas/MyData/types'; +import { THeaderProps } from 'shared/canvas/MyData/types'; type TSearchInputProps = Pick< - THeaderFilterProps, + THeaderProps, 'locate' | 'query' | 'setFilterOptions' >; const SearchInput = ({ @@ -16,11 +16,11 @@ const SearchInput = ({ const handleQueryChange: React.ChangeEventHandler<HTMLInputElement> = event => setFilterOptions({ query: event.target.value }); return ( - <div className="my-data-search-container"> + <div className="my-data-header-title_search"> <Input.Search allowClear className="my-data-search" - placeholder="Search dataset" + placeholder="Search for data" bordered={false} value={query} onChange={handleQueryChange} diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx index b1a7f3263..056fe04f3 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useRef, useState } from 'react'; import { NexusClient } from '@bbp/nexus-sdk'; import { useNexusContext } from '@bbp/react-nexus'; import { Checkbox, Col, Dropdown, Input, Row, Select } from 'antd'; -import { concat, isString, startCase } from 'lodash'; +import { isString, startCase } from 'lodash'; import { useQuery } from 'react-query'; import { THeaderProps, diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx index 30c50ffb3..e165e4f7d 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx @@ -1,27 +1,52 @@ +import * as pluralize from 'pluralize'; import TypeSelector from './TypeSelector'; import DateSelector from './DateSelector'; -import IssuerSelector from './IssuerSelector'; import DateFieldSelector from './DateFieldSelector'; +import PageTitle from './PageTitle'; +import IssuerSelector from './IssuerSelector'; import SearchInput from './SearchInput'; -import { THeaderFilterProps } from '../../../canvas/MyData/types'; +import { + THeaderFilterProps, + THeaderTitleProps, +} from '../../../canvas/MyData/types'; -const MyDataHeaderFilters = ({ - types, - dateField, +const MyDataHeaderTitle = ({ + total, query, + setFilterOptions, locate, issuer, +}: THeaderTitleProps) => { + return ( + <div className="my-data-header-title"> + <div className="left"> + <PageTitle + text="My data" + label={pluralize('Dataset', Number(total))} + total={total} + /> + <IssuerSelector {...{ issuer, setFilterOptions }} /> + </div> + <div className="right"> + <SearchInput {...{ query, locate, setFilterOptions }} /> + </div> + </div> + ); +}; + +const MyDataHeaderFilters = ({ + types, + dateField, setFilterOptions, }: THeaderFilterProps) => { return ( - <div className="my-data-table-header-actions"> - <IssuerSelector {...{ issuer, setFilterOptions }} /> + <div className="my-data-header-actions"> + <span className="filter-heading">Filter: </span> <DateFieldSelector {...{ dateField, setFilterOptions }} /> <DateSelector {...{ dateField, setFilterOptions }} /> <TypeSelector {...{ types, setFilterOptions }} /> - <SearchInput {...{ query, locate, setFilterOptions }} /> </div> ); }; -export default MyDataHeaderFilters; +export { MyDataHeaderFilters, MyDataHeaderTitle }; diff --git a/src/shared/molecules/MyDataHeader/styles.less b/src/shared/molecules/MyDataHeader/styles.less index 17d103eea..35ddfee7f 100644 --- a/src/shared/molecules/MyDataHeader/styles.less +++ b/src/shared/molecules/MyDataHeader/styles.less @@ -1,64 +1,159 @@ @import '../../lib.less'; -.my-data-table-header { +.my-data-header { display: flex; align-items: flex-start; justify-content: space-between; - flex-direction: column; + flex-flow: column nowrap; + margin-top: 15px; + .divider { + width: 100%; + height: 1px; + background-color: #e8e8e8; + margin: 30px 0 18px; + } +} + +.my-data-header-title { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + + .left { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + } + + .right { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 10px; + } +} + +.my-data-header-title_heading { + user-select: none; + margin-right: 20px; + + span:first-child { + font-weight: 700; + font-size: 30px; + line-height: 110%; + color: @fusion-main-color; + } + + span:last-child { + font-weight: 400; + font-size: 14px; + line-height: 22px; + letter-spacing: 0.01em; + color: #8c8c8c; + padding-left: 10px; + } +} + +.my-data-header-title_issuer_selector { + width: 100%; + max-width: max-content; + + .ant-radio + span { + color: @fusion-main-color; + } +} + +.my-data-header-title_search { + width: 100%; + min-width: 250px; + display: flex; + flex-direction: row; + align-items: flex-end; + gap: 10px; + + // margin-top: 52px; + .filter-options { + display: flex; + align-items: center; + justify-content: space-evenly; + column-gap: 10px; + width: 100%; - &-title { + .ant-checkbox-inner { + border-radius: 4px !important; + border-color: @fusion-main-color !important; + } + } + + .locate-text, + .spread-text { user-select: none; - margin: 20px 0 10px; - margin-right: 20px; + color: @fusion-main-color; + } - span:first-child { - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 700; - font-size: 28px; - line-height: 110%; - color: @fusion-main-color; + .my-data-search { + border-bottom: 1px solid @fusion-main-color !important; + min-width: 350px; + width: 100%; + + .ant-btn { + border: none; } - span:last-child { - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 22px; - letter-spacing: 0.01em; - color: #8c8c8c; - padding-left: 10px; + .anticon.anticon-search { + color: @fusion-main-color; } } +} - &-actions { - width: 100%; - margin: 0; - display: flex; - flex-flow: row wrap; - // grid-template-columns: max-content max-content 300px 300px 1fr; - gap: 15px; - align-items: center; - justify-content: flex-start; - align-content: center; +.my-data-header-actions { + width: 100%; + margin: 0; + display: flex; + flex-flow: row wrap; + gap: 15px; + align-items: center; + justify-content: flex-start; + align-content: center; + + .filter-heading { + font-weight: 400; + font-size: 14px; + line-height: 110%; + color: @fusion-neutral-7; } - .issuer-selector, + .date-field-selector { width: 100%; max-width: max-content; + + span { + color: @fusion-main-color; + } } - .my-data-date-picker, - .my-data-type-picker { + + .my-data-date-picker { width: 100%; max-width: 300px; + + input::placeholder, + input::-webkit-input-placeholder { + color: @fusion-main-color !important; + } } - .my-data-search-container { - flex: 0 1 25%; + + .my-data-type-picker { width: 100%; - min-width: 250px; + max-width: 300px; + + .ant-select-selection-placeholder { + color: @fusion-main-color !important; + } } } + .radio-filter { &.ant-radio-wrapper-checked { span:nth-child(2) { @@ -66,6 +161,7 @@ } } } + .date-type-selector { display: flex !important; align-items: center; @@ -73,6 +169,7 @@ gap: 15px; margin-bottom: 15px !important; } + .my-data-date { &-container { position: relative; @@ -107,10 +204,12 @@ align-items: center; justify-content: center; flex-direction: column; + p { font-size: 10px; color: red; } + .range-born { font-weight: 200; color: #40a9ff; @@ -154,22 +253,27 @@ .ant-select-selection-placeholder { color: @fusion-daybreak-10 !important; } + .ant-select-selection-overflow { flex-wrap: nowrap; overflow: scroll; + // style the scrollbar of this ant-select-selection-overflow &::-webkit-scrollbar { width: 2px; height: 2px; } + &::-webkit-scrollbar-thumb { background: @fusion-daybreak-10; } } } + .my-data-type-picker-popup { display: none; } + .my-data-type-filter { &-popover { background-color: white !important; @@ -177,33 +281,6 @@ } } -.my-data-search-container { - display: flex; - flex-direction: column; - align-items: flex-start; - column-gap: 5px; - // margin-top: 52px; - .filter-options { - display: flex; - align-items: center; - justify-content: space-evenly; - column-gap: 10px; - } - .locate-text, - .spread-text { - color: #8c8c8c; - } -} - -.my-data-search { - border-bottom: 1px solid @fusion-neutral-5 !important; - min-width: 350px; - width: 100%; - .ant-btn { - border: none; - } -} - .my-data-type-filter { &-overlay { background-color: white; @@ -225,11 +302,13 @@ -ms-overflow-style: none; scrollbar-width: none; + .no-types-content { display: flex; align-items: center; justify-content: center; height: 100%; + span { user-select: none; font-size: 12px; @@ -270,9 +349,11 @@ outline: none; box-shadow: none; } + .ant-input-affix-wrapper { border: none; background-color: transparent; + &-focused { box-shadow: none; outline: none; From 48563b219da566b5b2d0030134234fa97dd4ddce Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 13:00:48 +0200 Subject: [PATCH 170/192] f-3945/update: clean selectCallback fn type --- .../MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx index 056fe04f3..918579c40 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -93,10 +93,9 @@ const TypeSelector = ({ const [typeSearchValue, updateSearchType] = useState(''); const [typesOptionsArray, setTypesOptionsArray] = useState<TType[]>([]); - const selectCallback = useCallback((data: any) => { + const selectCallback = useCallback((data: TTypeAggregationsResult) => { const options = ( - ((data as unknown) as TTypeAggregationsResult).aggregations.types - ?.buckets ?? ([] as TTypesAggregatedBucket[]) + data.aggregations.types?.buckets ?? ([] as TTypesAggregatedBucket[]) ).map<TType>(item => typesOptionsBuilder(item)); originTypes.current = options; return options; From 26e02fac21428ea5f90e7e1db1dfabc11e5dbb8c Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 09:41:13 +0200 Subject: [PATCH 171/192] fix: replace the icon with highlight --- .../components/ResourceEditor/ResourceEditor.less | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 69f041ec0..fb0ad5d32 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -65,6 +65,10 @@ .fusion-resource-link { color: #0974ca !important; cursor: pointer !important; + background-color: rgba(#0974ca, 0.12); + border-radius: 4px; + padding: 1px; + border: 0.5px solid rgba(#0974ca, 0.14); &.wait-for-tooltip { cursor: progress !important; @@ -77,17 +81,6 @@ &.error { cursor: not-allowed !important; } - - &::after { - content: ''; - display: inline-block; - background-image: var(--resource-link-anchor-icon); - background-repeat: no-repeat; - background-size: 14px 14px; - width: 14px; - height: 14px; - margin-left: 10px; - } } .CodeMirror-lines { From 28874cd625cec1808fa183f130b4d6c669f0c2cb Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 15:54:54 +0200 Subject: [PATCH 172/192] hotfix: use option+click to download a file from url --- .../ResourceEditor/ResourceEditor.less | 4 +++ .../ResourceEditor/useEditorTooltip.tsx | 25 +++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/shared/components/ResourceEditor/ResourceEditor.less b/src/shared/components/ResourceEditor/ResourceEditor.less index 69f041ec0..816cd80df 100644 --- a/src/shared/components/ResourceEditor/ResourceEditor.less +++ b/src/shared/components/ResourceEditor/ResourceEditor.less @@ -213,6 +213,10 @@ margin-left: 10px; color: @fusion-primary-color; } + .key-binding { + margin-left: 10px; + color: @fusion-primary-color; + } } } diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index 5ce7a48a7..ca00c925f 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -56,12 +56,21 @@ function createTooltipNode({ nodeTitle.className = 'title'; nodeTitle.appendChild(document.createTextNode(title)); tooltipItemContent.appendChild(nodeTitle); - - const nodeDownload = isDownloadable && document.createElement('img'); - nodeDownload && nodeDownload.setAttribute('src', downloadImg); - nodeDownload && nodeDownload.classList.add('download-icon'); - nodeDownload && tooltipItemContent.appendChild(nodeDownload); - + if (isDownloadable) { + const nodeDownload = document.createElement('img'); + nodeDownload.setAttribute('src', downloadImg); + nodeDownload.classList.add('download-icon'); + tooltipItemContent.appendChild(nodeDownload); + 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') + ); + tooltipItemContent.appendChild(keyBinding); + } return tooltipItemContent; } function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { @@ -363,7 +372,9 @@ function useEditorPopover({ } case 'resource': { const result = results as TDELink; - if (result.isDownloadable) { + // this alt for windows, and option for mac + const optionClick = ev.altKey; + if (result.isDownloadable && optionClick) { return downloadBinaryAsyncHandler({ orgLabel: result.resource?.[0]!, projectLabel: result.resource?.[1]!, From f4f34fb59e5d74bf33539f40e75cff38ace5889c Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Wed, 19 Jul 2023 11:02:40 +0200 Subject: [PATCH 173/192] fix: remove calculating the digest after expand/shrink actions fix: remove existente tooltips on click --- .../ResourceEditor/useEditorTooltip.tsx | 19 ++++++++----------- src/shared/store/reducers/data-explorer.ts | 3 +-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index ca00c925f..ce5712105 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -105,9 +105,8 @@ function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { tooltipContent.appendChild( createTooltipNode({ tag: 'Multiple', - title: `${ - (results as TDELink[]).length - } resources was found, click to list them`, + title: `${(results as TDELink[]).length + } resources was found, click to list them`, }) ); return tooltipContent; @@ -190,11 +189,7 @@ function useEditorTooltip({ if (!tooltip.parentNode) { return; } - setTimeout(() => { - if (tooltip.parentNode) { - tooltip.parentNode.removeChild(tooltip); - } - }, 300); + tooltip.parentNode.removeChild(tooltip); } function showTooltip(content: HTMLDivElement, node: HTMLElement) { const tooltip = document.createElement('div'); @@ -223,7 +218,7 @@ function useEditorTooltip({ tooltip.remove(); } return clearTimeout(timeoutId); - }, 3000); + }, 2000); return tooltip; } @@ -256,8 +251,8 @@ function useEditorTooltip({ ? 'error' : resolvedAs === 'resource' && (results as TDELink).isDownloadable - ? 'downloadable' - : 'has-tooltip' + ? 'downloadable' + : 'has-tooltip' ); const tooltip = showTooltip(tooltipContent, node); const calculatePosition = (ev: MouseEvent) => @@ -286,6 +281,7 @@ function useEditorTooltip({ allowTooltip, ]); } + function useEditorPopover({ ref, orgLabel, @@ -341,6 +337,7 @@ function useEditorPopover({ ); } async function onMouseDown(_: CodeMirror.Editor, ev: MouseEvent) { + removeTooltipsFromDOM(); const node = ev.target as HTMLElement; if (node) { const { token } = getTokenAndPosAt(ev, currentEditor); diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 883d8eeb8..48847e877 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -301,7 +301,6 @@ export const dataExplorerSlice = createSlice({ ...state, ...sideUpdater, }; - calculateNewDigest(newState); return newState; }, ShrinkNavigationStackDataExplorerGraphFlow: (state, action) => { @@ -324,7 +323,6 @@ export const dataExplorerSlice = createSlice({ ...state, ...sideUpdater, }; - calculateNewDigest(newState); return newState; }, ResetDataExplorerGraphFlow: (_, action) => { @@ -343,6 +341,7 @@ export const dataExplorerSlice = createSlice({ }, }, }); + export const { PopulateDataExplorerGraphFlow, InitNewVisitDataExplorerGraphView, From 58a9e6f3c04e74276748d844be10144f2ef3142e Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Wed, 19 Jul 2023 12:25:18 +0200 Subject: [PATCH 174/192] fix: use the redux middleware listner to capture any action and save the new digest to sessionStorage --- .../DateExplorerGraphFlow.tsx | 16 +++++++ .../ResourceEditor/useEditorTooltip.tsx | 9 ++-- src/shared/store/index.ts | 4 +- src/shared/store/reducers/data-explorer.ts | 44 ++++++++++++++----- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index 20df74f14..d4a5720e9 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -8,6 +8,10 @@ import { DATA_EXPLORER_GRAPH_FLOW_PATH, PopulateDataExplorerGraphFlow, ResetDataExplorerGraphFlow, + DataExplorerFlowSliceListener, + DataExplorerMiddlewareMatcher, + calculateDateExplorerGraphFlowDigest, + TDataExplorerState, } from '../../store/reducers/data-explorer'; import { NavigationArrows, @@ -62,6 +66,18 @@ const DataExplorerGraphFlow = () => { }; }, [ResourceResolutionCache]); + useEffect(() => { + DataExplorerFlowSliceListener.startListening({ + matcher: DataExplorerMiddlewareMatcher, + effect: (_, api) => { + const state = (api.getState() as RootState).dataExplorer; + calculateDateExplorerGraphFlowDigest(state); + }, + }); + return () => { + DataExplorerFlowSliceListener.clearListeners(); + }; + }, []); return !current ? ( <DataExplorerGraphFlowEmpty /> ) : ( diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index ce5712105..9e06ade0d 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -105,8 +105,9 @@ function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { tooltipContent.appendChild( createTooltipNode({ tag: 'Multiple', - title: `${(results as TDELink[]).length - } resources was found, click to list them`, + title: `${ + (results as TDELink[]).length + } resources was found, click to list them`, }) ); return tooltipContent; @@ -251,8 +252,8 @@ function useEditorTooltip({ ? 'error' : resolvedAs === 'resource' && (results as TDELink).isDownloadable - ? 'downloadable' - : 'has-tooltip' + ? 'downloadable' + : 'has-tooltip' ); const tooltip = showTooltip(tooltipContent, node); const calculatePosition = (ev: MouseEvent) => diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 716fa1b81..4b263db7d 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -11,6 +11,7 @@ import { reducer as oidcReducer } from 'redux-oidc'; import { History } from 'history'; import { NexusClient } from '@bbp/nexus-sdk'; import reducers from './reducers'; +import { DataExplorerFlowSliceListener } from './reducers/data-explorer'; export type Services = { nexus: NexusClient; @@ -47,7 +48,8 @@ export default function configureStore( composeEnhancers( applyMiddleware( thunk.withExtraArgument({ nexus }), - routerMiddleware(history) + routerMiddleware(history), + DataExplorerFlowSliceListener.middleware ) ) ); diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 48847e877..456caf8d5 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -1,4 +1,8 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { + createListenerMiddleware, + createSlice, + isAnyOf, +} from '@reduxjs/toolkit'; import { slice, clone, @@ -70,7 +74,7 @@ const initialState: TDataExplorerState = { fullscreen: false, }; -const calculateNewDigest = (state: TDataExplorerState) => { +const calculateDateExplorerGraphFlowDigest = (state: TDataExplorerState) => { const clonedState = clone(state); const digest = btoa(JSON.stringify(clonedState)); sessionStorage.setItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST, digest); @@ -79,13 +83,13 @@ const calculateNewDigest = (state: TDataExplorerState) => { const isShrinkable = (links: TDELink[]) => { return links.length > MAX_NAVIGATION_ITEMS_IN_STACK; }; -function insert(arr: any[], index: number, item: any) { - arr.splice(index, 0, item); -} + +const DataExplorerFlowSliceName = 'data-explorer-graph-flow'; +const DataExplorerFlowSliceListener = createListenerMiddleware(); export const dataExplorerSlice = createSlice({ initialState, - name: 'data-explorer-graph-flow', + name: DataExplorerFlowSliceName, reducers: { PopulateDataExplorerGraphFlow: (state, action) => { const digest = action.payload; @@ -125,7 +129,7 @@ export const dataExplorerSlice = createSlice({ shrinked: false, }, }; - calculateNewDigest(newState); + calculateDateExplorerGraphFlowDigest(newState); return newState; }, AddNewNodeDataExplorerGraphFlow: (state, action) => { @@ -196,7 +200,7 @@ export const dataExplorerSlice = createSlice({ shrinked: isShrinkable(rightNodesLinks), }, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, JumpToNodeDataExplorerGraphFlow: (state, action) => { @@ -228,7 +232,7 @@ export const dataExplorerSlice = createSlice({ rightNodes, current, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, ReturnBackDataExplorerGraphFlow: state => { @@ -253,7 +257,7 @@ export const dataExplorerSlice = createSlice({ leftNodes, current: newCurrent, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, MoveForwardDataExplorerGraphFlow: state => { @@ -278,7 +282,7 @@ export const dataExplorerSlice = createSlice({ leftNodes, current: newCurrent, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, ExpandNavigationStackDataExplorerGraphFlow: (state, action) => { @@ -336,7 +340,7 @@ export const dataExplorerSlice = createSlice({ ...state, fullscreen: fullscreen ?? !state.fullscreen, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, }, @@ -355,4 +359,20 @@ export const { InitDataExplorerGraphFlowFullscreenVersion, } = dataExplorerSlice.actions; +const DataExplorerMiddlewareMatcher = isAnyOf( + InitNewVisitDataExplorerGraphView, + AddNewNodeDataExplorerGraphFlow, + ExpandNavigationStackDataExplorerGraphFlow, + ShrinkNavigationStackDataExplorerGraphFlow, + JumpToNodeDataExplorerGraphFlow, + ReturnBackDataExplorerGraphFlow, + MoveForwardDataExplorerGraphFlow, + InitDataExplorerGraphFlowFullscreenVersion +); +export { + DataExplorerFlowSliceName, + DataExplorerMiddlewareMatcher, + DataExplorerFlowSliceListener, + calculateDateExplorerGraphFlowDigest, +}; export default dataExplorerSlice.reducer; From f696825e5e36d976c7e3d8af7d71f03c95970208 Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Tue, 18 Jul 2023 09:56:47 +0200 Subject: [PATCH 175/192] 4041 // Works with non-collapsible header --- src/subapps/dataExplorer/DataExplorer.tsx | 108 ++++++++++-------- .../DataExplorerCollapsibleHeader.tsx | 52 +++++++++ .../dataExplorer/DataExplorerTable.tsx | 54 +++++---- src/subapps/dataExplorer/styles.less | 9 +- 4 files changed, 155 insertions(+), 68 deletions(-) create mode 100644 src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 66a692e3a..62064daff 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -1,5 +1,5 @@ import { Resource } from '@bbp/nexus-sdk'; -import { Switch } from 'antd'; +import { Spin, Switch } from 'antd'; import React, { useMemo, useReducer, useState } from 'react'; import { DataExplorerTable } from './DataExplorerTable'; import { @@ -13,6 +13,8 @@ import { PredicateSelector } from './PredicateSelector'; import { DatasetCount } from './DatasetCount'; import { TypeSelector } from './TypeSelector'; import './styles.less'; +import { DataExplorerCollapsibleHeader } from './DataExplorerCollapsibleHeader'; +import Loading from '../../shared/components/Loading'; export interface DataExplorerConfiguration { pageSize: number; @@ -26,6 +28,7 @@ export interface DataExplorerConfiguration { export const DataExplorer: React.FC<{}> = () => { const [showMetadataColumns, setShowMetadataColumns] = useState(false); const [showEmptyDataCells, setShowEmptyDataCells] = useState(true); + const [headerHeight, setHeaderHeight] = useState<number>(0); const [ { pageSize, offset, orgAndProject, predicate, type, selectedPath }, @@ -70,57 +73,67 @@ export const DataExplorer: React.FC<{}> = () => { return ( <div className="data-explorer-contents"> - <div className="data-explorer-filters"> - <ProjectSelector - onSelect={(orgLabel?: string, projectLabel?: string) => { - if (orgLabel && projectLabel) { - updateTableConfiguration({ - orgAndProject: [orgLabel, projectLabel], - }); - } else { - updateTableConfiguration({ orgAndProject: undefined }); - } + {isLoading ? ( + <Spin className="loading" /> + ) : ( + <DataExplorerCollapsibleHeader + onVisibilityChange={offsetHeight => { + setHeaderHeight(offsetHeight); }} - /> - <TypeSelector - orgAndProject={orgAndProject} - onSelect={selectedType => { - updateTableConfiguration({ type: selectedType }); - }} - /> - <PredicateSelector - dataSource={currentPageDataSource} - onPredicateChange={updateTableConfiguration} - /> - </div> - - {!isLoading && ( - <div className="flex-container"> - <DatasetCount - nexusTotal={resources?._total ?? 0} - totalOnPage={resources?._results?.length ?? 0} - totalFiltered={predicate ? displayedDataSource.length : undefined} - /> - <div className="data-explorer-toggles"> - <Switch - defaultChecked={false} - checked={showMetadataColumns} - onClick={isChecked => setShowMetadataColumns(isChecked)} - id="show-metadata-columns" - className="data-explorer-toggle" + > + <div className="data-explorer-filters"> + <ProjectSelector + onSelect={(orgLabel?: string, projectLabel?: string) => { + if (orgLabel && projectLabel) { + updateTableConfiguration({ + orgAndProject: [orgLabel, projectLabel], + }); + } else { + updateTableConfiguration({ orgAndProject: undefined }); + } + }} + /> + <TypeSelector + orgAndProject={orgAndProject} + onSelect={selectedType => { + updateTableConfiguration({ type: selectedType }); + }} /> - <label htmlFor="show-metadata-columns">Show metadata</label> + <PredicateSelector + dataSource={currentPageDataSource} + onPredicateChange={updateTableConfiguration} + /> + </div> - <Switch - defaultChecked={true} - checked={showEmptyDataCells} - onClick={isChecked => setShowEmptyDataCells(isChecked)} - id="show-empty-data-cells" - className="data-explorer-toggle" + <div className="flex-container"> + <DatasetCount + nexusTotal={resources?._total ?? 0} + totalOnPage={resources?._results?.length ?? 0} + totalFiltered={predicate ? displayedDataSource.length : undefined} /> - <label htmlFor="show-empty-data-cells">Show empty data cells</label> + <div className="data-explorer-toggles"> + <Switch + defaultChecked={false} + checked={showMetadataColumns} + onClick={isChecked => setShowMetadataColumns(isChecked)} + id="show-metadata-columns" + className="data-explorer-toggle" + /> + <label htmlFor="show-metadata-columns">Show metadata</label> + + <Switch + defaultChecked={true} + checked={showEmptyDataCells} + onClick={isChecked => setShowEmptyDataCells(isChecked)} + id="show-empty-data-cells" + className="data-explorer-toggle" + /> + <label htmlFor="show-empty-data-cells"> + Show empty data cells + </label> + </div> </div> - </div> + </DataExplorerCollapsibleHeader> )} <DataExplorerTable @@ -132,6 +145,7 @@ export const DataExplorer: React.FC<{}> = () => { offset={offset} updateTableConfiguration={updateTableConfiguration} showEmptyDataCells={showEmptyDataCells} + tableOffsetFromTop={headerHeight} /> </div> ); diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx new file mode 100644 index 000000000..8048930e9 --- /dev/null +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -0,0 +1,52 @@ +import React, { + CSSProperties, + ReactNode, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import './styles.less'; + +interface Props { + children: ReactNode; + onVisibilityChange: (offsetHeight: number) => void; +} + +export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ + children, + onVisibilityChange, +}: Props) => { + const [headerHeight, setHeaderHeight] = useState<number>(0); + const headerRef = useRef<HTMLDivElement>(null); + + useLayoutEffect(() => { + const headerY = + headerRef.current?.getBoundingClientRect().bottom ?? + FUSION_TITLEBAR_HEIGHT; + console.log('Calculated Height', headerY); + onVisibilityChange(headerY); + }, []); + + return ( + <> + <div + className="data-explorer-header" + style={{ ...fixedHeaderStyles }} + ref={headerRef} + > + {children} + </div> + </> + ); +}; + +export const FUSION_TITLEBAR_HEIGHT = 52; // height in pixels of the blue fixed header on every page of fusion. + +const fixedHeaderStyles: CSSProperties = { + position: 'fixed', + top: FUSION_TITLEBAR_HEIGHT, + left: 0, + width: '100vw', + padding: '20px 52px', + zIndex: 2, +}; diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index c2141c4e7..1feb58f26 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -20,6 +20,7 @@ interface TDataExplorerTable { updateTableConfiguration: React.Dispatch<Partial<DataExplorerConfiguration>>; columns: string[]; showEmptyDataCells: boolean; + tableOffsetFromTop: number; } type TColumnNameToConfig = Map<string, ColumnType<Resource>>; @@ -33,6 +34,7 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ offset, updateTableConfiguration, showEmptyDataCells, + tableOffsetFromTop, }: TDataExplorerTable) => { const history = useHistory(); const location = useLocation(); @@ -66,27 +68,39 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ }; return ( - <Table<Resource> - columns={columnsConfig(columns, showEmptyDataCells)} - dataSource={dataSource} - rowKey={record => record._self} - onRow={resource => ({ - onClick: _ => goToResource(resource), - 'data-testid': resource._self, - })} - loading={isLoading} - bordered={false} - className="data-explorer-table" - rowClassName="data-explorer-row" - scroll={{ x: 'max-content' }} - locale={{ - emptyText() { - return isLoading ? <></> : <Empty />; - }, + <div + style={{ + display: 'block', + position: 'absolute', + top: tableOffsetFromTop, + left: 0, + padding: '0 52px', + height: 'fit-content', + background: '#f5f5f5', }} - pagination={tablePaginationConfig} - sticky={{ offsetHeader: 52 }} - /> + > + <Table<Resource> + columns={columnsConfig(columns, showEmptyDataCells)} + dataSource={dataSource} + rowKey={record => record._self} + onRow={resource => ({ + onClick: _ => goToResource(resource), + 'data-testid': resource._self, + })} + loading={isLoading} + bordered={false} + className="data-explorer-table" + rowClassName="data-explorer-row" + scroll={{ x: 'max-content' }} + locale={{ + emptyText() { + return isLoading ? <></> : <Empty />; + }, + }} + pagination={tablePaginationConfig} + sticky={{ offsetHeader: tableOffsetFromTop }} + /> + </div> ); }; diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 4dd17543b..f59ebc9fa 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -7,6 +7,14 @@ width: 100%; margin-top: 100px; + .loading { + margin: 0 auto; + } + + .data-explorer-header { + background: white; + } + .data-explorer-filters { display: flex; margin-bottom: 28px; @@ -14,7 +22,6 @@ .flex-container { display: flex; - max-width: 90vw; justify-content: space-between; align-items: center; margin-bottom: 20px; From 443b7230ea843345d340013821b62fee41dc05cb Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Tue, 18 Jul 2023 10:18:47 +0200 Subject: [PATCH 176/192] 4041 // Removes header when scrolling down does not reveal again --- .../DataExplorerCollapsibleHeader.tsx | 79 ++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx index 8048930e9..8f06c2606 100644 --- a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -16,32 +16,56 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ children, onVisibilityChange, }: Props) => { - const [headerHeight, setHeaderHeight] = useState<number>(0); + const [headerBottom, setHeaderBottom] = useState<number>(0); + const [headerOutOfViewport, setHeaderOutOfViewport] = useState(false); + const headerRef = useRef<HTMLDivElement>(null); useLayoutEffect(() => { const headerY = headerRef.current?.getBoundingClientRect().bottom ?? FUSION_TITLEBAR_HEIGHT; - console.log('Calculated Height', headerY); + console.log('Setting header bootom', headerY); + setHeaderBottom(headerY); onVisibilityChange(headerY); }, []); + useScrollPosition( + (currentYPosition: number) => { + console.log('Header Bottom in UseScrollPosition', headerBottom); + const shouldHide = currentYPosition > headerBottom; + if (shouldHide !== headerOutOfViewport) { + toggleHeaderVisibility(shouldHide); + } + }, + 100, // throttle time in ms for scroll event + [headerBottom] + ); + + const toggleHeaderVisibility = (shouldHide: boolean) => { + setHeaderOutOfViewport(shouldHide); + console.log('Should Show', shouldHide, headerBottom); + onVisibilityChange(shouldHide ? FUSION_TITLEBAR_HEIGHT : headerBottom); + }; + return ( <> - <div - className="data-explorer-header" - style={{ ...fixedHeaderStyles }} - ref={headerRef} - > - {children} - </div> + {!headerOutOfViewport && ( + <div + className="data-explorer-header" + style={{ ...fixedHeaderStyles }} + ref={headerRef} + > + {children} + </div> + )} </> ); }; export const FUSION_TITLEBAR_HEIGHT = 52; // height in pixels of the blue fixed header on every page of fusion. +// TODO: Move to styles.less const fixedHeaderStyles: CSSProperties = { position: 'fixed', top: FUSION_TITLEBAR_HEIGHT, @@ -50,3 +74,40 @@ const fixedHeaderStyles: CSSProperties = { padding: '20px 52px', zIndex: 2, }; + +const isBrowser = typeof window !== `undefined`; + +const getScrollYPosition = (): number => { + if (!isBrowser) return 0; + + return window.scrollY; +}; + +export function useScrollPosition( + effect: (currentYPosition: number) => void, + waitMs: number, + deps: React.DependencyList +) { + const yPosition = useRef(getScrollYPosition()); + + let throttleTimeout: number | null = null; + + const callBack = () => { + const currentPosition = getScrollYPosition(); + effect(currentPosition); + yPosition.current = currentPosition; + throttleTimeout = null; + }; + + useLayoutEffect(() => { + const handleScroll = () => { + if (throttleTimeout === null) { + throttleTimeout = window.setTimeout(callBack, waitMs); + } + }; + + window.addEventListener('scroll', handleScroll); + + return () => window.removeEventListener('scroll', handleScroll); + }, deps); +} From 1ff5f74de40e173d4d85c3c789cd547ec518b467 Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Tue, 18 Jul 2023 10:31:18 +0200 Subject: [PATCH 177/192] 4041 // Header reveals again when user scrolls up --- src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx index 8f06c2606..38d3b4ad0 100644 --- a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -25,26 +25,23 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ const headerY = headerRef.current?.getBoundingClientRect().bottom ?? FUSION_TITLEBAR_HEIGHT; - console.log('Setting header bootom', headerY); setHeaderBottom(headerY); onVisibilityChange(headerY); }, []); useScrollPosition( - (currentYPosition: number) => { - console.log('Header Bottom in UseScrollPosition', headerBottom); + function(currentYPosition: number) { const shouldHide = currentYPosition > headerBottom; if (shouldHide !== headerOutOfViewport) { toggleHeaderVisibility(shouldHide); } }, 100, // throttle time in ms for scroll event - [headerBottom] + [headerBottom, headerOutOfViewport] ); const toggleHeaderVisibility = (shouldHide: boolean) => { setHeaderOutOfViewport(shouldHide); - console.log('Should Show', shouldHide, headerBottom); onVisibilityChange(shouldHide ? FUSION_TITLEBAR_HEIGHT : headerBottom); }; From 1b82023e9b307c7eb76544d444070e1b6ff8d776 Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Tue, 18 Jul 2023 10:50:57 +0200 Subject: [PATCH 178/192] 4041 // Clicking on expand button expands the header --- src/shared/components/Icons/FilterIcon.tsx | 20 ++++++++++++++++ .../DataExplorerCollapsibleHeader.tsx | 24 +++++++++++++++++-- src/subapps/dataExplorer/styles.less | 7 ++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/shared/components/Icons/FilterIcon.tsx diff --git a/src/shared/components/Icons/FilterIcon.tsx b/src/shared/components/Icons/FilterIcon.tsx new file mode 100644 index 000000000..c306e5cdb --- /dev/null +++ b/src/shared/components/Icons/FilterIcon.tsx @@ -0,0 +1,20 @@ +export const FilterIcon = () => { + return ( + <svg + width="14" + height="10" + viewBox="0 0 14 10" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M4.76248 0.5C4.28394 0.5 3.81976 0.66248 3.44521 0.959719C3.07071 1.25745 2.80766 1.67308 2.69916 2.13874H1.14614C0.877143 2.13874 0.65918 2.35672 0.65918 2.6257C0.65918 2.89468 0.877156 3.11317 1.14614 3.11317H2.69916C2.84729 3.73686 3.26936 4.25999 3.84797 4.53646C4.42608 4.81288 5.09833 4.81288 5.67695 4.53646C6.25507 4.26003 6.67713 3.7369 6.82526 3.11317H12.8541C13.1231 3.11317 13.3411 2.8947 13.3411 2.6257C13.3411 2.35671 13.1231 2.13874 12.8541 2.13874H6.82526C6.71677 1.67308 6.45372 1.25745 6.07921 0.959719C5.7047 0.66248 5.24055 0.5 4.76248 0.5ZM4.76248 3.77752C4.29881 3.77752 3.8807 3.49862 3.70334 3.07011C3.526 2.6421 3.62408 2.14919 3.95202 1.82124C4.27947 1.4933 4.77288 1.3952 5.2009 1.57256C5.62941 1.7499 5.90831 2.16801 5.90831 2.6317C5.90831 2.93537 5.78793 3.22715 5.57293 3.44215C5.35794 3.65714 5.06615 3.77752 4.76248 3.77752Z" + fill="#003A8C" + /> + <path + d="M9.25517 5.25622C8.77663 5.25672 8.31294 5.4187 7.93841 5.71644C7.56341 6.01367 7.30085 6.4293 7.19236 6.89496H1.14614C0.877143 6.89496 0.65918 7.11344 0.65918 7.38243C0.65918 7.65143 0.877156 7.86939 1.14614 7.86939H7.17499C7.32311 8.49309 7.74518 9.01621 8.32329 9.29268C8.90191 9.56911 9.57415 9.56911 10.1523 9.29268C10.7309 9.01625 11.153 8.49312 11.3011 7.86939H12.8541C13.1231 7.86939 13.3411 7.65141 13.3411 7.38243C13.3411 7.11345 13.1231 6.89496 12.8541 6.89496H11.3011C11.1931 6.43228 10.9325 6.01913 10.5615 5.72189C10.1904 5.42465 9.73077 5.26069 9.25517 5.25622ZM9.25517 8.52283C8.7915 8.52283 8.37388 8.24343 8.19653 7.81542C8.01869 7.38691 8.11677 6.894 8.44471 6.56605C8.77265 6.2386 9.26557 6.14051 9.69359 6.31786C10.1221 6.4952 10.4015 6.91282 10.4015 7.37651C10.4015 7.68067 10.2806 7.97196 10.0656 8.18696C9.85063 8.40195 9.55934 8.52283 9.25517 8.52283Z" + fill="#003A8C" + /> + </svg> + ); +}; diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx index 38d3b4ad0..25d88dcde 100644 --- a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -6,6 +6,8 @@ import React, { useState, } from 'react'; import './styles.less'; +import { Button } from 'antd'; +import { FilterIcon } from '../../shared/components/Icons/FilterIcon'; interface Props { children: ReactNode; @@ -16,8 +18,9 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ children, onVisibilityChange, }: Props) => { - const [headerBottom, setHeaderBottom] = useState<number>(0); + const [headerBottom, setHeaderBottom] = useState(0); const [headerOutOfViewport, setHeaderOutOfViewport] = useState(false); + const [headerExpanded, setHeaderExpanded] = useState(false); const headerRef = useRef<HTMLDivElement>(null); @@ -35,6 +38,9 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ if (shouldHide !== headerOutOfViewport) { toggleHeaderVisibility(shouldHide); } + if (!headerOutOfViewport) { + setHeaderExpanded(false); + } }, 100, // throttle time in ms for scroll event [headerBottom, headerOutOfViewport] @@ -47,7 +53,7 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ return ( <> - {!headerOutOfViewport && ( + {(!headerOutOfViewport || headerExpanded) && ( <div className="data-explorer-header" style={{ ...fixedHeaderStyles }} @@ -56,6 +62,20 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ {children} </div> )} + + {headerOutOfViewport && ( + <> + <Button + icon={<FilterIcon />} + onClick={() => { + setHeaderExpanded(true); + onVisibilityChange(headerBottom); + }} + shape="circle" + className="toggle-header-buttons" + /> + </> + )} </> ); }; diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index f59ebc9fa..645c33162 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -11,6 +11,13 @@ margin: 0 auto; } + .toggle-header-buttons { + z-index: 5; + position: fixed; + top: 100px; + left: 20px; + } + .data-explorer-header { background: white; } From 43ffe34bd5120ec556622e0f8daf19498c8adfad Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Tue, 18 Jul 2023 11:05:26 +0200 Subject: [PATCH 179/192] 4041 // Header collapses when clicked on close button --- .../DataExplorerCollapsibleHeader.tsx | 32 +++++++++++++------ src/subapps/dataExplorer/styles.less | 7 +++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx index 25d88dcde..6421af157 100644 --- a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -8,6 +8,7 @@ import React, { import './styles.less'; import { Button } from 'antd'; import { FilterIcon } from '../../shared/components/Icons/FilterIcon'; +import { CloseOutlined } from '@ant-design/icons'; interface Props { children: ReactNode; @@ -65,15 +66,28 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ {headerOutOfViewport && ( <> - <Button - icon={<FilterIcon />} - onClick={() => { - setHeaderExpanded(true); - onVisibilityChange(headerBottom); - }} - shape="circle" - className="toggle-header-buttons" - /> + {headerExpanded ? ( + <Button + icon={<CloseOutlined />} + onClick={() => { + setHeaderExpanded(false); + onVisibilityChange(FUSION_TITLEBAR_HEIGHT); + }} + shape="circle" + className="toggle-header-buttons" + style={{ top: 60, left: 10 }} + /> + ) : ( + <Button + icon={<FilterIcon />} + onClick={() => { + setHeaderExpanded(true); + onVisibilityChange(headerBottom); + }} + shape="circle" + className="toggle-header-buttons" + /> + )} </> )} </> diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 645c33162..836fe41be 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -16,6 +16,11 @@ position: fixed; top: 100px; left: 20px; + color: @fusion-blue-8; + &:active, &:focus { + border-color: @fusion-gray-4; + color: @fusion-blue-8; + } } .data-explorer-header { @@ -102,7 +107,7 @@ input { color: @fusion-blue-8 !important; border: none; - background: @fusion-main-bg; + background: transparent; font-weight: 700; &::placeholder { From dd4d947b4e61a33512abf7d49a19d9f0e93389ac Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Tue, 18 Jul 2023 11:10:41 +0200 Subject: [PATCH 180/192] 4041 // Move styles to less file --- .../DataExplorerCollapsibleHeader.tsx | 24 ++----------------- src/subapps/dataExplorer/styles.less | 6 +++++ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx index 6421af157..e25fd22cb 100644 --- a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -1,10 +1,4 @@ -import React, { - CSSProperties, - ReactNode, - useLayoutEffect, - useRef, - useState, -} from 'react'; +import React, { ReactNode, useLayoutEffect, useRef, useState } from 'react'; import './styles.less'; import { Button } from 'antd'; import { FilterIcon } from '../../shared/components/Icons/FilterIcon'; @@ -55,11 +49,7 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ return ( <> {(!headerOutOfViewport || headerExpanded) && ( - <div - className="data-explorer-header" - style={{ ...fixedHeaderStyles }} - ref={headerRef} - > + <div className="data-explorer-header" ref={headerRef}> {children} </div> )} @@ -96,16 +86,6 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ export const FUSION_TITLEBAR_HEIGHT = 52; // height in pixels of the blue fixed header on every page of fusion. -// TODO: Move to styles.less -const fixedHeaderStyles: CSSProperties = { - position: 'fixed', - top: FUSION_TITLEBAR_HEIGHT, - left: 0, - width: '100vw', - padding: '20px 52px', - zIndex: 2, -}; - const isBrowser = typeof window !== `undefined`; const getScrollYPosition = (): number => { diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 836fe41be..3af1b30a4 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -25,6 +25,12 @@ .data-explorer-header { background: white; + position: fixed; + top: 52px; // Fusion titlebar height + left: 0; + width: 100vw; + padding: 20px 52px; + z-index: 2, } .data-explorer-filters { From cd2659c74b12f1fc5644348be885e055dd44150c Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Tue, 18 Jul 2023 13:05:11 +0200 Subject: [PATCH 181/192] 4041 // add tests --- src/subapps/dataExplorer/DataExplorer.spec.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 4c7d09950..f73fc11a1 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -838,4 +838,19 @@ describe('DataExplorer', () => { ); expect(textForSpecialPropertyAfter).toMatch(''); }); + + it('show data explorer header by default', async () => { + // const pro = await getProjectAutocomplete(); + // expect(pro).toBeVisible(); + const type = await getInputForLabel(TypeMenuLabel); + expect(type).toBeVisible(); + const predicate = await getInputForLabel(PredicateMenuLabel); + expect(predicate).toBeVisible(); + const totalResultsCount = await getTotalSizeOfDataset('500,123'); + expect(totalResultsCount).toBeVisible(); + const metadataSwitch = await showMetadataSwitch(); + expect(metadataSwitch).toBeVisible(); + const showEmptyCellsToggle = await showEmptyDataCellsSwitch(); + expect(showEmptyCellsToggle).toBeVisible(); + }); }); From 84c804e2a4b1e976a870e1afbf29530d77269593 Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Tue, 18 Jul 2023 17:03:07 +0200 Subject: [PATCH 182/192] US 4041 // Add tests Signed-off-by: Dinika Saxena <dinikasaxenas@gmail.com> --- .../dataExplorer/DataExplorer.spec.tsx | 168 ++++++++++++++++-- src/subapps/dataExplorer/DataExplorer.tsx | 113 ++++++------ .../DataExplorerCollapsibleHeader.tsx | 21 ++- .../dataExplorer/DataExplorerTable.tsx | 5 +- src/subapps/dataExplorer/ProjectSelector.tsx | 1 + src/subapps/dataExplorer/styles.less | 9 +- 6 files changed, 227 insertions(+), 90 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index f73fc11a1..9e351a112 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -1,7 +1,14 @@ import { Resource, createNexusClient } from '@bbp/nexus-sdk'; import { NexusProvider } from '@bbp/react-nexus'; import '@testing-library/jest-dom'; -import { RenderResult, act, fireEvent, within } from '@testing-library/react'; +import { + RenderResult, + act, + fireEvent, + queryByRole, + waitForElementToBeRemoved, + within, +} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import { @@ -77,6 +84,7 @@ describe('DataExplorer', () => { container = component.container; user = userEvent.setup(); + await expectRowCountToBe(mockResourcesOnPage1.length); }); afterEach(async () => { @@ -106,6 +114,24 @@ describe('DataExplorer', () => { }); }; + const waitForHeaderToBeHidden = async () => { + return await waitFor(() => { + const dataExplorerHeader = document.querySelector( + '.data-explorer-header' + ) as HTMLDivElement; + expect(dataExplorerHeader).not.toBeVisible(); + }); + }; + + const waitForHeaderToBeVisible = async () => { + return await waitFor(() => { + const dataExplorerHeader = document.querySelector( + '.data-explorer-header' + ) as HTMLDivElement; + expect(dataExplorerHeader).toBeVisible(); + }); + }; + const expectColumHeaderToExist = async (name: string) => { const nameReg = new RegExp(getColumnTitle(name), 'i'); const header = await screen.getByText(nameReg, { @@ -337,6 +363,49 @@ describe('DataExplorer', () => { } }; + const expectDataExplorerHeaderToExist = async () => { + const pro = await getProjectAutocomplete(); + expect(pro).toBeVisible(); + const type = await getInputForLabel(TypeMenuLabel); + expect(type).toBeVisible(); + const predicate = await getInputForLabel(PathMenuLabel); + expect(predicate).toBeVisible(); + const totalResultsCount = await getTotalSizeOfDataset('500,123'); + expect(totalResultsCount).toBeVisible(); + const metadataSwitch = await showMetadataSwitch(); + expect(metadataSwitch).toBeVisible(); + const showEmptyCellsToggle = await showEmptyDataCellsSwitch(); + expect(showEmptyCellsToggle).toBeVisible(); + }; + + const scrollWindow = async (yPosition: number) => { + await fireEvent.scroll(window, { target: { scrollY: yPosition } }); + }; + + const getButtonByLabel = async (label: string) => { + const buttonElement = await screen.getByRole('button', { + name: label, + }); + return buttonElement; + }; + + const expandHeaderButton = async () => + await getButtonByLabel('expand-header'); + + const collapseHeaderButton = async () => + await getButtonByLabel('collapse-header'); + + const clickExpandHeaderButton = async () => { + const expandHeaderButtonElement = await expandHeaderButton(); + + await userEvent.click(expandHeaderButtonElement); + }; + + const clickCollapseHeaderButton = async () => { + const collapseHeaderButtonElement = await collapseHeaderButton(); + await userEvent.click(collapseHeaderButtonElement); + }; + it('shows columns for fields that are only in source data', async () => { await expectRowCountToBe(10); const column = await expectColumHeaderToExist('userProperty1'); @@ -530,8 +599,6 @@ describe('DataExplorer', () => { }); it('resets selected project when user clicks reset button', async () => { - await expectRowCountToBe(10); - await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); expect(visibleTableRows().length).toBeLessThan(10); @@ -542,8 +609,6 @@ describe('DataExplorer', () => { }); it('shows all projects when allProjects option is selected', async () => { - await expectRowCountToBe(10); - await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); expect(visibleTableRows().length).toBeLessThan(10); @@ -573,6 +638,8 @@ describe('DataExplorer', () => { }); it('only shows types that exist in selected project in type autocomplete', async () => { + await expectRowCountToBe(10); + await openMenuFor(TypeMenuLabel); const optionBefore = await getDropdownOption( 'Dataset', @@ -581,6 +648,7 @@ describe('DataExplorer', () => { expect(optionBefore).toBeInTheDocument(); await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); + await expectRowCountToBe(2); await openMenuFor(TypeMenuLabel); expect( @@ -840,17 +908,83 @@ describe('DataExplorer', () => { }); it('show data explorer header by default', async () => { - // const pro = await getProjectAutocomplete(); - // expect(pro).toBeVisible(); - const type = await getInputForLabel(TypeMenuLabel); - expect(type).toBeVisible(); - const predicate = await getInputForLabel(PredicateMenuLabel); - expect(predicate).toBeVisible(); - const totalResultsCount = await getTotalSizeOfDataset('500,123'); - expect(totalResultsCount).toBeVisible(); - const metadataSwitch = await showMetadataSwitch(); - expect(metadataSwitch).toBeVisible(); - const showEmptyCellsToggle = await showEmptyDataCellsSwitch(); - expect(showEmptyCellsToggle).toBeVisible(); + await expectDataExplorerHeaderToExist(); + }); + + it('hides data explorer header when user scrolls past its height', async () => { + await expectDataExplorerHeaderToExist(); + + await scrollWindow(500); + await waitForHeaderToBeHidden(); + + expect(expectDataExplorerHeaderToExist()).rejects.toThrow(); + }); + + it('shows expand header button when data explorer is not visible', async () => { + await scrollWindow(500); + await waitForHeaderToBeHidden(); + + await clickExpandHeaderButton(); + + await expectDataExplorerHeaderToExist(); + }); + + it('collapses header again when collapse button is clicked', async () => { + await scrollWindow(500); + await waitForHeaderToBeHidden(); + + await clickExpandHeaderButton(); + await expectDataExplorerHeaderToExist(); + + await clickCollapseHeaderButton(); + expect(expectDataExplorerHeaderToExist()).rejects.toThrow(); + }); + + it('hides expand header button when user scrolls up', async () => { + await scrollWindow(500); + await waitForHeaderToBeHidden(); + + expect(await expandHeaderButton()).toBeVisible(); + + await scrollWindow(0); + await waitForHeaderToBeVisible(); + + expect(expandHeaderButton()).rejects.toThrow(); + }); + + it('hides collapse header button when user scrolls up', async () => { + await scrollWindow(500); + await waitForHeaderToBeHidden(); + + await clickExpandHeaderButton(); + expect(await collapseHeaderButton()).toBeVisible(); + + await scrollWindow(0); + await waitForHeaderToBeVisible(); + + await waitForElementToBeRemoved(() => + screen.queryByRole('button', { name: 'collapse-header' }) + ); + }); + + it('does not reset values in filters when header was hidden due to scroll', async () => { + await selectOptionFromMenu(ProjectMenuLabel, 'unhcr'); + await selectOptionFromMenu(TypeMenuLabel, 'file', CustomOptionSelector); + await selectPath('@type'); + + await scrollWindow(500); + await waitForHeaderToBeHidden(); + + await scrollWindow(0); + await waitForHeaderToBeVisible(); + + const projectInput = await getInputForLabel(ProjectMenuLabel); + expect(projectInput.value).toMatch(new RegExp('unhcr', 'i')); + + const typeInput = await getSelectedValueInMenu(TypeMenuLabel); + expect(typeInput).toMatch(new RegExp('file', 'i')); + + const pathInput = await getSelectedValueInMenu(PathMenuLabel); + expect(pathInput).toMatch(new RegExp('@type', 'i')); }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 62064daff..dda2c2a9d 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -73,69 +73,64 @@ export const DataExplorer: React.FC<{}> = () => { return ( <div className="data-explorer-contents"> - {isLoading ? ( - <Spin className="loading" /> - ) : ( - <DataExplorerCollapsibleHeader - onVisibilityChange={offsetHeight => { - setHeaderHeight(offsetHeight); - }} - > - <div className="data-explorer-filters"> - <ProjectSelector - onSelect={(orgLabel?: string, projectLabel?: string) => { - if (orgLabel && projectLabel) { - updateTableConfiguration({ - orgAndProject: [orgLabel, projectLabel], - }); - } else { - updateTableConfiguration({ orgAndProject: undefined }); - } - }} - /> - <TypeSelector - orgAndProject={orgAndProject} - onSelect={selectedType => { - updateTableConfiguration({ type: selectedType }); - }} - /> - <PredicateSelector - dataSource={currentPageDataSource} - onPredicateChange={updateTableConfiguration} - /> - </div> + {isLoading && <Spin className="loading" />} - <div className="flex-container"> - <DatasetCount - nexusTotal={resources?._total ?? 0} - totalOnPage={resources?._results?.length ?? 0} - totalFiltered={predicate ? displayedDataSource.length : undefined} + <DataExplorerCollapsibleHeader + onVisibilityChange={offsetHeight => { + setHeaderHeight(offsetHeight); + }} + > + <div className="data-explorer-filters"> + <ProjectSelector + onSelect={(orgLabel?: string, projectLabel?: string) => { + if (orgLabel && projectLabel) { + updateTableConfiguration({ + orgAndProject: [orgLabel, projectLabel], + }); + } else { + updateTableConfiguration({ orgAndProject: undefined }); + } + }} + /> + <TypeSelector + orgAndProject={orgAndProject} + onSelect={selectedType => { + updateTableConfiguration({ type: selectedType }); + }} + /> + <PredicateSelector + dataSource={currentPageDataSource} + onPredicateChange={updateTableConfiguration} + /> + </div> + + <div className="flex-container"> + <DatasetCount + nexusTotal={resources?._total ?? 0} + totalOnPage={resources?._results?.length ?? 0} + totalFiltered={predicate ? displayedDataSource.length : undefined} + /> + <div className="data-explorer-toggles"> + <Switch + defaultChecked={false} + checked={showMetadataColumns} + onClick={isChecked => setShowMetadataColumns(isChecked)} + id="show-metadata-columns" + className="data-explorer-toggle" /> - <div className="data-explorer-toggles"> - <Switch - defaultChecked={false} - checked={showMetadataColumns} - onClick={isChecked => setShowMetadataColumns(isChecked)} - id="show-metadata-columns" - className="data-explorer-toggle" - /> - <label htmlFor="show-metadata-columns">Show metadata</label> + <label htmlFor="show-metadata-columns">Show metadata</label> - <Switch - defaultChecked={true} - checked={showEmptyDataCells} - onClick={isChecked => setShowEmptyDataCells(isChecked)} - id="show-empty-data-cells" - className="data-explorer-toggle" - /> - <label htmlFor="show-empty-data-cells"> - Show empty data cells - </label> - </div> + <Switch + defaultChecked={true} + checked={showEmptyDataCells} + onClick={isChecked => setShowEmptyDataCells(isChecked)} + id="show-empty-data-cells" + className="data-explorer-toggle" + /> + <label htmlFor="show-empty-data-cells">Show empty data cells</label> </div> - </DataExplorerCollapsibleHeader> - )} - + </div> + </DataExplorerCollapsibleHeader> <DataExplorerTable isLoading={isLoading} dataSource={displayedDataSource} diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx index e25fd22cb..c540c8328 100644 --- a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -28,7 +28,7 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ }, []); useScrollPosition( - function(currentYPosition: number) { + (currentYPosition: number) => { const shouldHide = currentYPosition > headerBottom; if (shouldHide !== headerOutOfViewport) { toggleHeaderVisibility(shouldHide); @@ -48,12 +48,15 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ return ( <> - {(!headerOutOfViewport || headerExpanded) && ( - <div className="data-explorer-header" ref={headerRef}> - {children} - </div> - )} - + <div + className="data-explorer-header" + ref={headerRef} + style={{ + display: !headerOutOfViewport || headerExpanded ? 'block' : 'none', + }} + > + {children} + </div> {headerOutOfViewport && ( <> {headerExpanded ? ( @@ -65,6 +68,7 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ }} shape="circle" className="toggle-header-buttons" + aria-label="collapse-header" style={{ top: 60, left: 10 }} /> ) : ( @@ -75,6 +79,7 @@ export const DataExplorerCollapsibleHeader: React.FC<Props> = ({ onVisibilityChange(headerBottom); }} shape="circle" + aria-label="expand-header" className="toggle-header-buttons" /> )} @@ -88,7 +93,7 @@ export const FUSION_TITLEBAR_HEIGHT = 52; // height in pixels of the blue fixed const isBrowser = typeof window !== `undefined`; -const getScrollYPosition = (): number => { +export const getScrollYPosition = (): number => { if (!isBrowser) return 0; return window.scrollY; diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index 1feb58f26..52788b54e 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -75,8 +75,9 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ top: tableOffsetFromTop, left: 0, padding: '0 52px', - height: 'fit-content', background: '#f5f5f5', + height: 'fit-content', + minHeight: '100%', }} > <Table<Resource> @@ -87,7 +88,7 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ onClick: _ => goToResource(resource), 'data-testid': resource._self, })} - loading={isLoading} + loading={{ spinning: isLoading, indicator: <></> }} bordered={false} className="data-explorer-table" rowClassName="data-explorer-row" diff --git a/src/subapps/dataExplorer/ProjectSelector.tsx b/src/subapps/dataExplorer/ProjectSelector.tsx index 173ad87e7..f180597d3 100644 --- a/src/subapps/dataExplorer/ProjectSelector.tsx +++ b/src/subapps/dataExplorer/ProjectSelector.tsx @@ -51,6 +51,7 @@ export const ProjectSelector: React.FC<Props> = ({ onSelect }: Props) => { bordered={false} className="search-input" popupClassName="search-menu" + data-testid="project-filter" > <Input size="middle" diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 3af1b30a4..606a28868 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -8,7 +8,7 @@ margin-top: 100px; .loading { - margin: 0 auto; + margin: 40vh auto; } .toggle-header-buttons { @@ -17,7 +17,8 @@ top: 100px; left: 20px; color: @fusion-blue-8; - &:active, &:focus { + &:active, + &:focus { border-color: @fusion-gray-4; color: @fusion-blue-8; } @@ -30,7 +31,7 @@ left: 0; width: 100vw; padding: 20px 52px; - z-index: 2, + z-index: 2; } .data-explorer-filters { @@ -210,7 +211,7 @@ .data-explorer-table { table { width: auto; - min-width: 90vw; + min-width: unset !important; } .ant-table { background: @fusion-main-bg; From bb7d0960d6e8d6d863418b33d8b16f97bfe25afc Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Wed, 19 Jul 2023 10:20:12 +0200 Subject: [PATCH 183/192] 4041 // Add shadow to header & white background to toggle buttons --- src/subapps/dataExplorer/DataExplorerTable.tsx | 8 +++++++- src/subapps/dataExplorer/styles.less | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/subapps/dataExplorer/DataExplorerTable.tsx b/src/subapps/dataExplorer/DataExplorerTable.tsx index 52788b54e..f742c1f7e 100644 --- a/src/subapps/dataExplorer/DataExplorerTable.tsx +++ b/src/subapps/dataExplorer/DataExplorerTable.tsx @@ -10,6 +10,8 @@ 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'; interface TDataExplorerTable { isLoading: boolean; @@ -90,7 +92,11 @@ export const DataExplorerTable: React.FC<TDataExplorerTable> = ({ })} loading={{ spinning: isLoading, indicator: <></> }} bordered={false} - className="data-explorer-table" + className={clsx( + 'data-explorer-table', + tableOffsetFromTop === FUSION_TITLEBAR_HEIGHT && + 'data-explorer-header-collapsed' + )} rowClassName="data-explorer-row" scroll={{ x: 'max-content' }} locale={{ diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 606a28868..fce495cf6 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -17,8 +17,10 @@ top: 100px; left: 20px; color: @fusion-blue-8; + background: white; &:active, &:focus { + background: white; border-color: @fusion-gray-4; color: @fusion-blue-8; } @@ -208,6 +210,12 @@ } } +.data-explorer-header-collapsed { + .ant-table-header { + box-shadow: 4px 4px 4px 0px rgba(0, 0, 0, 0.08); + } +} + .data-explorer-table { table { width: auto; @@ -216,6 +224,7 @@ .ant-table { background: @fusion-main-bg; } + .ant-table-thead > tr > th.data-explorer-column { background-color: #f5f5f5 !important; font-family: 'Titillium Web'; From f5a6680ac50168db06c4fa064d4ed72ecc765819 Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Wed, 19 Jul 2023 10:34:56 +0200 Subject: [PATCH 184/192] 4041 // Resets predicate value when new predicate is selected --- src/subapps/dataExplorer/DataExplorer.spec.tsx | 15 +++++++++++++++ src/subapps/dataExplorer/PredicateSelector.tsx | 10 ++++------ src/subapps/dataExplorer/styles.less | 4 +++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 9e351a112..75c240b89 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -987,4 +987,19 @@ describe('DataExplorer', () => { const pathInput = await getSelectedValueInMenu(PathMenuLabel); expect(pathInput).toMatch(new RegExp('@type', 'i')); }); + + it('resets predicate search term when different predicate verb is selected', async () => { + await updateResourcesShownInTable(mockResourcesForPage2); + await selectPath('author'); + await selectPredicate(CONTAINS); + const valueInput = await screen.getByPlaceholderText('Search for...'); + await userEvent.type(valueInput, 'iggy'); + + await selectPredicate(EXISTS); + + await selectPredicate(DOES_NOT_CONTAIN); + + const valueInputAfter = await screen.getByPlaceholderText('Search for...'); + expect((valueInputAfter as HTMLInputElement).value).not.toEqual('iggy'); + }); }); diff --git a/src/subapps/dataExplorer/PredicateSelector.tsx b/src/subapps/dataExplorer/PredicateSelector.tsx index 6208c688c..5155761f5 100644 --- a/src/subapps/dataExplorer/PredicateSelector.tsx +++ b/src/subapps/dataExplorer/PredicateSelector.tsx @@ -156,10 +156,12 @@ export const PredicateSelector: React.FC<Props> = ({ options={predicateFilterOptions} onSelect={(predicateLabel: PredicateFilterOptions['value']) => { setFormField(PREDICATE_FIELD, predicateLabel); + setFormField(SEARCH_TERM_FIELD, ''); + predicateSelected( getFormFieldValue(PATH_FIELD), predicateLabel, - getFormFieldValue(SEARCH_TERM_FIELD) + '' ); }} aria-label="predicate-selector" @@ -168,11 +170,7 @@ export const PredicateSelector: React.FC<Props> = ({ autoFocus={true} allowClear={true} onClear={() => { - predicateSelected( - getFormFieldValue(PATH_FIELD), - null, - getFormFieldValue(SEARCH_TERM_FIELD) - ); + predicateSelected(getFormFieldValue(PATH_FIELD), null, ''); }} /> </Form.Item> diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index fce495cf6..2602bbe90 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -9,6 +9,8 @@ .loading { margin: 40vh auto; + z-index: 100; + position: relative; } .toggle-header-buttons { @@ -205,7 +207,7 @@ .predicate-value-input { color: @fusion-blue-8; - background: @fusion-main-bg; + background: transparent; width: 200px; } } From 6ed364d7c65369994e9d473eca2f68880fdadfb0 Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Wed, 19 Jul 2023 10:38:19 +0200 Subject: [PATCH 185/192] 4041 // Clear timeout in useLayout cleanup --- src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx index c540c8328..a7996b151 100644 --- a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -124,6 +124,11 @@ export function useScrollPosition( window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + if (throttleTimeout) { + window.clearTimeout(throttleTimeout); + } + }; }, deps); } From c51ff11605784210189d23ed96a80506f3fcc87e Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Wed, 19 Jul 2023 11:47:16 +0200 Subject: [PATCH 186/192] 4041 // Table body width should match table header width when fewer columns are available (example when File type is selected) --- src/subapps/dataExplorer/styles.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index 2602bbe90..bfc3d3ce8 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -221,7 +221,7 @@ .data-explorer-table { table { width: auto; - min-width: unset !important; + 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; From 7d61895a1fa1f1567ad47c89a03632ac77299747 Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Wed, 19 Jul 2023 12:03:57 +0200 Subject: [PATCH 187/192] 4041 // Use throttle to limit scroll listener --- .../dataExplorer/DataExplorer.spec.tsx | 7 ++----- .../DataExplorerCollapsibleHeader.tsx | 20 ++++++------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 75c240b89..fcdb9cf06 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -960,11 +960,8 @@ describe('DataExplorer', () => { expect(await collapseHeaderButton()).toBeVisible(); await scrollWindow(0); - await waitForHeaderToBeVisible(); - - await waitForElementToBeRemoved(() => - screen.queryByRole('button', { name: 'collapse-header' }) - ); + // await waitForHeaderToBeVisible(); + expect(collapseHeaderButton()).rejects.toThrow(); }); it('does not reset values in filters when header was hidden due to scroll', async () => { diff --git a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx index a7996b151..2c9e77777 100644 --- a/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx +++ b/src/subapps/dataExplorer/DataExplorerCollapsibleHeader.tsx @@ -3,6 +3,7 @@ import './styles.less'; import { Button } from 'antd'; import { FilterIcon } from '../../shared/components/Icons/FilterIcon'; import { CloseOutlined } from '@ant-design/icons'; +import { throttle } from 'lodash'; interface Props { children: ReactNode; @@ -106,29 +107,20 @@ export function useScrollPosition( ) { const yPosition = useRef(getScrollYPosition()); - let throttleTimeout: number | null = null; - const callBack = () => { const currentPosition = getScrollYPosition(); effect(currentPosition); yPosition.current = currentPosition; - throttleTimeout = null; }; useLayoutEffect(() => { - const handleScroll = () => { - if (throttleTimeout === null) { - throttleTimeout = window.setTimeout(callBack, waitMs); - } - }; - - window.addEventListener('scroll', handleScroll); + const throttledCallback = throttle(callBack, waitMs, { + leading: true, + }); + window.addEventListener('scroll', throttledCallback); return () => { - window.removeEventListener('scroll', handleScroll); - if (throttleTimeout) { - window.clearTimeout(throttleTimeout); - } + window.removeEventListener('scroll', throttledCallback); }; }, deps); } From 6f21766d6245f651d46b9f493a71b8cf88956cfc Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Wed, 19 Jul 2023 12:21:35 +0200 Subject: [PATCH 188/192] 4041 // Remove max worker in jest because they cause issues in ci Signed-off-by: Dinika Saxena <dinikasaxenas@gmail.com> --- package.json | 7 ++----- src/subapps/dataExplorer/DataExplorer.spec.tsx | 1 - src/subapps/dataExplorer/styles.less | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 7df9091c1..e1a1e38df 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", "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,10 +211,7 @@ ], "globals": { "FUSION_VERSION": "1.0.0", - "COMMIT_HASH": "9013fa343", - "ts-jest": { - "isolatedModules": true - } + "COMMIT_HASH": "9013fa343" }, "watchPathIgnorePatterns": [ "node_modules" diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index fcdb9cf06..7b72f386b 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -960,7 +960,6 @@ describe('DataExplorer', () => { expect(await collapseHeaderButton()).toBeVisible(); await scrollWindow(0); - // await waitForHeaderToBeVisible(); expect(collapseHeaderButton()).rejects.toThrow(); }); diff --git a/src/subapps/dataExplorer/styles.less b/src/subapps/dataExplorer/styles.less index bfc3d3ce8..7ef14836f 100644 --- a/src/subapps/dataExplorer/styles.less +++ b/src/subapps/dataExplorer/styles.less @@ -221,7 +221,7 @@ .data-explorer-table { 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). + 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; From 8ff6503e6ad6f29b9b64374304ccb33a9ccd44a0 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Wed, 19 Jul 2023 11:02:40 +0200 Subject: [PATCH 189/192] fix: remove calculating the digest after expand/shrink actions fix: remove existente tooltips on click --- .../ResourceEditor/useEditorTooltip.tsx | 19 ++++++++----------- src/shared/store/reducers/data-explorer.ts | 3 +-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index ca00c925f..ce5712105 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -105,9 +105,8 @@ function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { tooltipContent.appendChild( createTooltipNode({ tag: 'Multiple', - title: `${ - (results as TDELink[]).length - } resources was found, click to list them`, + title: `${(results as TDELink[]).length + } resources was found, click to list them`, }) ); return tooltipContent; @@ -190,11 +189,7 @@ function useEditorTooltip({ if (!tooltip.parentNode) { return; } - setTimeout(() => { - if (tooltip.parentNode) { - tooltip.parentNode.removeChild(tooltip); - } - }, 300); + tooltip.parentNode.removeChild(tooltip); } function showTooltip(content: HTMLDivElement, node: HTMLElement) { const tooltip = document.createElement('div'); @@ -223,7 +218,7 @@ function useEditorTooltip({ tooltip.remove(); } return clearTimeout(timeoutId); - }, 3000); + }, 2000); return tooltip; } @@ -256,8 +251,8 @@ function useEditorTooltip({ ? 'error' : resolvedAs === 'resource' && (results as TDELink).isDownloadable - ? 'downloadable' - : 'has-tooltip' + ? 'downloadable' + : 'has-tooltip' ); const tooltip = showTooltip(tooltipContent, node); const calculatePosition = (ev: MouseEvent) => @@ -286,6 +281,7 @@ function useEditorTooltip({ allowTooltip, ]); } + function useEditorPopover({ ref, orgLabel, @@ -341,6 +337,7 @@ function useEditorPopover({ ); } async function onMouseDown(_: CodeMirror.Editor, ev: MouseEvent) { + removeTooltipsFromDOM(); const node = ev.target as HTMLElement; if (node) { const { token } = getTokenAndPosAt(ev, currentEditor); diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 883d8eeb8..48847e877 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -301,7 +301,6 @@ export const dataExplorerSlice = createSlice({ ...state, ...sideUpdater, }; - calculateNewDigest(newState); return newState; }, ShrinkNavigationStackDataExplorerGraphFlow: (state, action) => { @@ -324,7 +323,6 @@ export const dataExplorerSlice = createSlice({ ...state, ...sideUpdater, }; - calculateNewDigest(newState); return newState; }, ResetDataExplorerGraphFlow: (_, action) => { @@ -343,6 +341,7 @@ export const dataExplorerSlice = createSlice({ }, }, }); + export const { PopulateDataExplorerGraphFlow, InitNewVisitDataExplorerGraphView, From 1c76c9f755f23ef71c3a5307071538c0020887be Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Wed, 19 Jul 2023 12:25:18 +0200 Subject: [PATCH 190/192] fix: use the redux middleware listner to capture any action and save the new digest to sessionStorage --- .../DateExplorerGraphFlow.tsx | 16 +++++++ .../ResourceEditor/useEditorTooltip.tsx | 9 ++-- src/shared/store/index.ts | 4 +- src/shared/store/reducers/data-explorer.ts | 44 ++++++++++++++----- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx index 20df74f14..d4a5720e9 100644 --- a/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx +++ b/src/shared/canvas/DataExplorerGraphFlow/DateExplorerGraphFlow.tsx @@ -8,6 +8,10 @@ import { DATA_EXPLORER_GRAPH_FLOW_PATH, PopulateDataExplorerGraphFlow, ResetDataExplorerGraphFlow, + DataExplorerFlowSliceListener, + DataExplorerMiddlewareMatcher, + calculateDateExplorerGraphFlowDigest, + TDataExplorerState, } from '../../store/reducers/data-explorer'; import { NavigationArrows, @@ -62,6 +66,18 @@ const DataExplorerGraphFlow = () => { }; }, [ResourceResolutionCache]); + useEffect(() => { + DataExplorerFlowSliceListener.startListening({ + matcher: DataExplorerMiddlewareMatcher, + effect: (_, api) => { + const state = (api.getState() as RootState).dataExplorer; + calculateDateExplorerGraphFlowDigest(state); + }, + }); + return () => { + DataExplorerFlowSliceListener.clearListeners(); + }; + }, []); return !current ? ( <DataExplorerGraphFlowEmpty /> ) : ( diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index ce5712105..9e06ade0d 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -105,8 +105,9 @@ function createTooltipContent({ resolvedAs, error, results }: TTooltipCreator) { tooltipContent.appendChild( createTooltipNode({ tag: 'Multiple', - title: `${(results as TDELink[]).length - } resources was found, click to list them`, + title: `${ + (results as TDELink[]).length + } resources was found, click to list them`, }) ); return tooltipContent; @@ -251,8 +252,8 @@ function useEditorTooltip({ ? 'error' : resolvedAs === 'resource' && (results as TDELink).isDownloadable - ? 'downloadable' - : 'has-tooltip' + ? 'downloadable' + : 'has-tooltip' ); const tooltip = showTooltip(tooltipContent, node); const calculatePosition = (ev: MouseEvent) => diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 716fa1b81..4b263db7d 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -11,6 +11,7 @@ import { reducer as oidcReducer } from 'redux-oidc'; import { History } from 'history'; import { NexusClient } from '@bbp/nexus-sdk'; import reducers from './reducers'; +import { DataExplorerFlowSliceListener } from './reducers/data-explorer'; export type Services = { nexus: NexusClient; @@ -47,7 +48,8 @@ export default function configureStore( composeEnhancers( applyMiddleware( thunk.withExtraArgument({ nexus }), - routerMiddleware(history) + routerMiddleware(history), + DataExplorerFlowSliceListener.middleware ) ) ); diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 48847e877..456caf8d5 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -1,4 +1,8 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { + createListenerMiddleware, + createSlice, + isAnyOf, +} from '@reduxjs/toolkit'; import { slice, clone, @@ -70,7 +74,7 @@ const initialState: TDataExplorerState = { fullscreen: false, }; -const calculateNewDigest = (state: TDataExplorerState) => { +const calculateDateExplorerGraphFlowDigest = (state: TDataExplorerState) => { const clonedState = clone(state); const digest = btoa(JSON.stringify(clonedState)); sessionStorage.setItem(DATA_EXPLORER_GRAPH_FLOW_DIGEST, digest); @@ -79,13 +83,13 @@ const calculateNewDigest = (state: TDataExplorerState) => { const isShrinkable = (links: TDELink[]) => { return links.length > MAX_NAVIGATION_ITEMS_IN_STACK; }; -function insert(arr: any[], index: number, item: any) { - arr.splice(index, 0, item); -} + +const DataExplorerFlowSliceName = 'data-explorer-graph-flow'; +const DataExplorerFlowSliceListener = createListenerMiddleware(); export const dataExplorerSlice = createSlice({ initialState, - name: 'data-explorer-graph-flow', + name: DataExplorerFlowSliceName, reducers: { PopulateDataExplorerGraphFlow: (state, action) => { const digest = action.payload; @@ -125,7 +129,7 @@ export const dataExplorerSlice = createSlice({ shrinked: false, }, }; - calculateNewDigest(newState); + calculateDateExplorerGraphFlowDigest(newState); return newState; }, AddNewNodeDataExplorerGraphFlow: (state, action) => { @@ -196,7 +200,7 @@ export const dataExplorerSlice = createSlice({ shrinked: isShrinkable(rightNodesLinks), }, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, JumpToNodeDataExplorerGraphFlow: (state, action) => { @@ -228,7 +232,7 @@ export const dataExplorerSlice = createSlice({ rightNodes, current, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, ReturnBackDataExplorerGraphFlow: state => { @@ -253,7 +257,7 @@ export const dataExplorerSlice = createSlice({ leftNodes, current: newCurrent, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, MoveForwardDataExplorerGraphFlow: state => { @@ -278,7 +282,7 @@ export const dataExplorerSlice = createSlice({ leftNodes, current: newCurrent, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, ExpandNavigationStackDataExplorerGraphFlow: (state, action) => { @@ -336,7 +340,7 @@ export const dataExplorerSlice = createSlice({ ...state, fullscreen: fullscreen ?? !state.fullscreen, }; - calculateNewDigest(newState); + // calculateNewDigest(newState); return newState; }, }, @@ -355,4 +359,20 @@ export const { InitDataExplorerGraphFlowFullscreenVersion, } = dataExplorerSlice.actions; +const DataExplorerMiddlewareMatcher = isAnyOf( + InitNewVisitDataExplorerGraphView, + AddNewNodeDataExplorerGraphFlow, + ExpandNavigationStackDataExplorerGraphFlow, + ShrinkNavigationStackDataExplorerGraphFlow, + JumpToNodeDataExplorerGraphFlow, + ReturnBackDataExplorerGraphFlow, + MoveForwardDataExplorerGraphFlow, + InitDataExplorerGraphFlowFullscreenVersion +); +export { + DataExplorerFlowSliceName, + DataExplorerMiddlewareMatcher, + DataExplorerFlowSliceListener, + calculateDateExplorerGraphFlowDigest, +}; export default dataExplorerSlice.reducer; From 7e699ecf36a5c7a23da46259475ac0d1b8d37690 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Wed, 19 Jul 2023 16:05:44 +0200 Subject: [PATCH 191/192] fix: clean code --- src/shared/components/ResourceEditor/useEditorTooltip.tsx | 2 +- src/shared/store/reducers/data-explorer.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/shared/components/ResourceEditor/useEditorTooltip.tsx b/src/shared/components/ResourceEditor/useEditorTooltip.tsx index 9e06ade0d..7560d9765 100644 --- a/src/shared/components/ResourceEditor/useEditorTooltip.tsx +++ b/src/shared/components/ResourceEditor/useEditorTooltip.tsx @@ -219,7 +219,7 @@ function useEditorTooltip({ tooltip.remove(); } return clearTimeout(timeoutId); - }, 2000); + }, 3000); return tooltip; } diff --git a/src/shared/store/reducers/data-explorer.ts b/src/shared/store/reducers/data-explorer.ts index 456caf8d5..0c39bb652 100644 --- a/src/shared/store/reducers/data-explorer.ts +++ b/src/shared/store/reducers/data-explorer.ts @@ -200,7 +200,6 @@ export const dataExplorerSlice = createSlice({ shrinked: isShrinkable(rightNodesLinks), }, }; - // calculateNewDigest(newState); return newState; }, JumpToNodeDataExplorerGraphFlow: (state, action) => { @@ -232,7 +231,6 @@ export const dataExplorerSlice = createSlice({ rightNodes, current, }; - // calculateNewDigest(newState); return newState; }, ReturnBackDataExplorerGraphFlow: state => { @@ -257,7 +255,6 @@ export const dataExplorerSlice = createSlice({ leftNodes, current: newCurrent, }; - // calculateNewDigest(newState); return newState; }, MoveForwardDataExplorerGraphFlow: state => { @@ -282,7 +279,6 @@ export const dataExplorerSlice = createSlice({ leftNodes, current: newCurrent, }; - // calculateNewDigest(newState); return newState; }, ExpandNavigationStackDataExplorerGraphFlow: (state, action) => { @@ -340,7 +336,6 @@ export const dataExplorerSlice = createSlice({ ...state, fullscreen: fullscreen ?? !state.fullscreen, }; - // calculateNewDigest(newState); return newState; }, }, From f3816774a0b67da01d5a049804fcc69d0669c71e Mon Sep 17 00:00:00 2001 From: Dinika Saxena <dinikasaxenas@gmail.com> Date: Wed, 19 Jul 2023 16:34:02 +0200 Subject: [PATCH 192/192] Hotfix // Fix antd autoscroll warning --- src/subapps/dataExplorer/ProjectSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subapps/dataExplorer/ProjectSelector.tsx b/src/subapps/dataExplorer/ProjectSelector.tsx index f180597d3..4d7535501 100644 --- a/src/subapps/dataExplorer/ProjectSelector.tsx +++ b/src/subapps/dataExplorer/ProjectSelector.tsx @@ -46,7 +46,6 @@ export const ProjectSelector: React.FC<Props> = ({ onSelect }: Props) => { allowClear={true} clearIcon={<CloseOutlined data-testid="reset-project-button" />} onClear={() => onSelect(undefined, undefined)} - placeholder={AllProjects} aria-label="project-filter" bordered={false} className="search-input" @@ -56,6 +55,7 @@ export const ProjectSelector: React.FC<Props> = ({ onSelect }: Props) => { <Input size="middle" addonAfter={showClearIcon ? <CloseOutlined /> : <SearchOutlined />} + placeholder={AllProjects} // Antd doesn't like adding placeholder along with 'allowClear' in AutoComplete element, so it needs to be added to this Input element. (https://github.com/ant-design/ant-design/issues/26760). /> </AutoComplete> </div>