From 6e78769394d28b61d658c23667c35d363e72a1c5 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 16 Oct 2023 17:19:49 +0100 Subject: [PATCH 1/5] Move taxonomy filters above text search --- .../common/browse-controls/index.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/scripts/components/common/browse-controls/index.tsx b/app/scripts/components/common/browse-controls/index.tsx index 245fef34d..dab0c28f0 100644 --- a/app/scripts/components/common/browse-controls/index.tsx +++ b/app/scripts/components/common/browse-controls/index.tsx @@ -85,6 +85,20 @@ function BrowseControls(props: BrowseControlsProps) { return ( + + {taxonomiesOptions.map(({ name, values }) => ( + { + onAction(Actions.TAXONOMY, { key: name, value: v }); + }} + size={isLargeUp ? 'large' : 'medium'} + /> + ))} + - - {taxonomiesOptions.map(({ name, values }) => ( - { - onAction(Actions.TAXONOMY, { key: name, value: v }); - }} - size={isLargeUp ? 'large' : 'medium'} - /> - ))} - ); } From 1263fc891fb08dd6d3b85db147e135ce03b0a04f Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 16 Oct 2023 17:20:18 +0100 Subject: [PATCH 2/5] Add onLinkClick to ElementInteractive --- app/scripts/components/common/card.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/scripts/components/common/card.tsx b/app/scripts/components/common/card.tsx index 85e69af2e..94c5addfb 100644 --- a/app/scripts/components/common/card.tsx +++ b/app/scripts/components/common/card.tsx @@ -299,6 +299,7 @@ interface CardComponentProps { parentTo?: string; footerContent?: ReactNode; onCardClickCapture?: MouseEventHandler; + onLinkClick?: MouseEventHandler; } function CardComponent(props: CardComponentProps) { @@ -316,7 +317,8 @@ function CardComponent(props: CardComponentProps) { parentName, parentTo, footerContent, - onCardClickCapture + onCardClickCapture, + onLinkClick } = props; return ( @@ -327,7 +329,8 @@ function CardComponent(props: CardComponentProps) { linkLabel={linkLabel || 'View more'} linkProps={{ as: Link, - to: linkTo + to: linkTo, + onClick: onLinkClick }} onClickCapture={onCardClickCapture} > From cd205d55e9a05b3479599b9fb813fa68c5f2aeab Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 16 Oct 2023 17:20:37 +0100 Subject: [PATCH 3/5] Convert EmptyHub to styled component --- app/scripts/components/common/empty-hub.tsx | 26 +++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/scripts/components/common/empty-hub.tsx b/app/scripts/components/common/empty-hub.tsx index 060168767..1260744d3 100644 --- a/app/scripts/components/common/empty-hub.tsx +++ b/app/scripts/components/common/empty-hub.tsx @@ -5,7 +5,20 @@ import { themeVal } from '@devseed-ui/theme-provider'; import { variableGlsp } from '$styles/variable-utils'; -const EmptyHubWrapper = styled.div` +function EmptyHub(props: { children: ReactNode }) { + const theme = useTheme(); + + const { children, ...rest } = props; + + return ( +
+ + {children} +
+ ); +} + +export default styled(EmptyHub)` max-width: 100%; grid-column: 1/-1; display: flex; @@ -16,14 +29,3 @@ const EmptyHubWrapper = styled.div` border: 1px dashed ${themeVal('color.base-300')}; gap: ${variableGlsp(1)}; `; - -export default function EmptyHub(props: { children: ReactNode }) { - const theme = useTheme(); - - return ( - - - {props.children} - - ); -} \ No newline at end of file From c54dbe275d5bfbe84a002e7cd922c2edd6acf53c Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 16 Oct 2023 17:21:14 +0100 Subject: [PATCH 4/5] Implement analysis dataset selector modal --- .../browse-controls/use-browse-controls.ts | 7 +- app/scripts/components/data-catalog/index.tsx | 9 +- .../components/dataset-selector-modal.tsx | 384 +++++++++++++++--- .../components/exploration/data-utils.ts | 2 + app/scripts/styles/theme.ts | 2 +- 5 files changed, 336 insertions(+), 68 deletions(-) diff --git a/app/scripts/components/common/browse-controls/use-browse-controls.ts b/app/scripts/components/common/browse-controls/use-browse-controls.ts index 51c0664b7..c973fc74b 100644 --- a/app/scripts/components/common/browse-controls/use-browse-controls.ts +++ b/app/scripts/components/common/browse-controls/use-browse-controls.ts @@ -4,13 +4,14 @@ import useQsStateCreator from 'qs-state-hook'; import { set, omit } from 'lodash'; export enum Actions { + CLEAR = 'clear', SEARCH = 'search', SORT_FIELD = 'sfield', SORT_DIR = 'sdir', TAXONOMY = 'taxonomy' } -export type BrowserControlsAction = (what: Actions, value: any) => void; +export type BrowserControlsAction = (what: Actions, value?: any) => void; export interface FilterOption { id: string; @@ -84,6 +85,10 @@ export function useBrowserControls({ sortOptions }: BrowseControlsHookParams) { const onAction = useCallback( (what, value) => { switch (what) { + case Actions.CLEAR: + setSearch(''); + setTaxonomies({}); + break; case Actions.SEARCH: setSearch(value); break; diff --git a/app/scripts/components/data-catalog/index.tsx b/app/scripts/components/data-catalog/index.tsx index 56b5e99a2..7af9992fe 100644 --- a/app/scripts/components/data-catalog/index.tsx +++ b/app/scripts/components/data-catalog/index.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useRef } from 'react'; import styled from 'styled-components'; -import { DatasetData, datasets, datasetTaxonomies, getString } from 'veda'; +import { DatasetData, datasetTaxonomies, getString } from 'veda'; import { Link } from 'react-router-dom'; import { glsp } from '@devseed-ui/theme-provider'; import { Subtitle } from '@devseed-ui/typography'; @@ -47,8 +47,7 @@ import { TAXONOMY_TOPICS } from '$utils/veda-data'; import { DatasetClassification } from '$components/common/dataset-classification'; - -const allDatasets = Object.values(datasets).map((d) => d!.data); +import { allDatasets } from '$components/exploration/data-utils'; const DatasetCount = styled(Subtitle)` grid-column: 1 / -1; @@ -66,9 +65,9 @@ const BrowseFoldHeader = styled(FoldHeader)` align-items: flex-start; `; -const sortOptions = [{ id: 'name', name: 'Name' }]; +export const sortOptions = [{ id: 'name', name: 'Name' }]; -const prepareDatasets = ( +export const prepareDatasets = ( data: DatasetData[], options: { search: string; diff --git a/app/scripts/components/exploration/components/dataset-selector-modal.tsx b/app/scripts/components/exploration/components/dataset-selector-modal.tsx index 12befd3f1..aff8e78f1 100644 --- a/app/scripts/components/exploration/components/dataset-selector-modal.tsx +++ b/app/scripts/components/exploration/components/dataset-selector-modal.tsx @@ -1,39 +1,165 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import styled from 'styled-components'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import styled, { css } from 'styled-components'; import { useAtom } from 'jotai'; -import { Modal } from '@devseed-ui/modal'; -import { media, themeVal } from '@devseed-ui/theme-provider'; -import { Form, FormCheckable } from '@devseed-ui/form'; -import { Overline } from '@devseed-ui/typography'; +import { DatasetData, DatasetLayer, datasetTaxonomies } from 'veda'; +import { Modal, ModalBody, ModalFooter } from '@devseed-ui/modal'; +import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { Button } from '@devseed-ui/button'; +import { Subtitle } from '@devseed-ui/typography'; +import { + CollecticonTickSmall, + CollecticonXmarkSmall, + iconDataURI +} from '@devseed-ui/collecticons'; import { timelineDatasetsAtom } from '../atoms/atoms'; -import { datasetLayers, findParentDataset, reconcileDatasets } from '../data-utils'; +import { + allDatasets, + datasetLayers, + findParentDataset, + reconcileDatasets +} from '../data-utils'; -import { variableGlsp } from '$styles/variable-utils'; +import EmptyHub from '$components/common/empty-hub'; +import { + Card, + CardList, + CardMeta, + CardTopicsList +} from '$components/common/card'; +import { DatasetClassification } from '$components/common/dataset-classification'; +import { CardSourcesList } from '$components/common/card-sources'; +import DatasetMenu from '$components/data-catalog/dataset-menu'; +import { getDatasetPath } from '$utils/routes'; +import { + getTaxonomy, + TAXONOMY_SOURCE, + TAXONOMY_TOPICS +} from '$utils/veda-data'; +import { Pill } from '$styles/pill'; +import BrowseControls from '$components/common/browse-controls'; +import { + Actions, + useBrowserControls +} from '$components/common/browse-controls/use-browse-controls'; +import { prepareDatasets, sortOptions } from '$components/data-catalog'; +import Pluralize from '$utils/pluralize'; -const CheckableGroup = styled.div` - display: grid; - gap: ${variableGlsp(0.5)}; - grid-template-columns: repeat(2, 1fr); - background: ${themeVal('color.surface')}; +const DatasetModal = styled(Modal)` + z-index: ${themeVal('zIndices.modal')}; - ${media.mediumUp` - grid-template-columns: repeat(3, 1fr); - `} + /* Override ModalContents */ + > div { + height: calc(100vh - ${glsp(4)}); + display: flex; + flex-flow: column; + } - ${media.xlargeUp` - grid-template-columns: repeat(4, 1fr); - `} + ${ModalBody} { + height: 100%; + min-height: 0; + display: flex; + flex-flow: column; + gap: ${glsp(1)}; + } + + ${ModalFooter} { + display: flex; + gap: ${glsp(1)}; + align-items: center; + + > .selection-info { + margin-right: auto; + } + } +`; + +const ModalIntro = styled.div` + margin-bottom: ${glsp(2)}; +`; + +const DatasetCount = styled(Subtitle)` + display: flex; + gap: ${glsp(0.5)}; + + span { + text-transform: uppercase; + line-height: 1.5rem; + } `; -const FormCheckableCustom = styled(FormCheckable)` - padding: ${variableGlsp(0.5)}; - background: ${themeVal('color.surface')}; - box-shadow: 0 0 0 1px ${themeVal('color.base-100a')}; - border-radius: ${themeVal('shape.rounded')}; - align-items: center; +const DatasetContainer = styled.div` + height: 100%; + min-height: 0; + display: flex; + margin: ${glsp(0, -2)}; + box-shadow: 0 -1px 0 0 ${themeVal('color.base-100a')}, + inset 0 -1px 0 0 ${themeVal('color.base-100a')}; + + ${CardList} { + overflow-y: auto; + padding: ${glsp(2)}; + } + + ${EmptyHub} { + flex-grow: 1; + } +`; + +const LayerCard = styled(Card)<{ checked: boolean }>` + outline: 4px solid transparent; + ${({ checked }) => + checked && + css` + outline-color: ${themeVal('color.primary')}; + `} + + &:hover { + &::before, + &::after { + opacity: 1; + } + } + + &::before, + &::after { + display: block; + content: ''; + position: absolute; + transition: opacity 320ms ease-in-out; + opacity: 0.32; + } + + &::before { + top: 0; + right: 0; + width: 4rem; + height: 4rem; + clip-path: polygon(0 0, 100% 0, 100% 100%); + background: ${themeVal('color.primary')}; + } + + &::after { + top: 0.25rem; + right: 0.25rem; + width: 1.5rem; + height: 1.5rem; + background-image: url(${({ theme }) => + iconDataURI(CollecticonTickSmall, { + color: theme.color?.surface, + size: 'large' + })}); + } + + ${({ checked }) => + checked && + css` + &::before, + &::after { + opacity: 1; + } + `} `; interface DatasetSelectorModalProps { @@ -44,16 +170,16 @@ interface DatasetSelectorModalProps { export function DatasetSelectorModal(props: DatasetSelectorModalProps) { const { revealed, close } = props; - const [datasets, setDatasets] = useAtom(timelineDatasetsAtom); + const [timelineDatasets, setTimelineDatasets] = useAtom(timelineDatasetsAtom); // Store a list of selected datasets and only confirm on save. const [selectedIds, setSelectedIds] = useState( - datasets.map((dataset) => dataset.data.id) + timelineDatasets.map((dataset) => dataset.data.id) ); useEffect(() => { - setSelectedIds(datasets.map((dataset) => dataset.data.id)); - }, [datasets]); + setSelectedIds(timelineDatasets.map((dataset) => dataset.data.id)); + }, [timelineDatasets]); const onCheck = useCallback((id) => { setSelectedIds((ids) => @@ -63,50 +189,186 @@ export function DatasetSelectorModal(props: DatasetSelectorModalProps) { const onConfirm = useCallback(() => { // Reconcile selectedIds with datasets. - setDatasets(reconcileDatasets(selectedIds, datasetLayers, datasets)); + setTimelineDatasets( + reconcileDatasets(selectedIds, datasetLayers, timelineDatasets) + ); close(); - }, [close, selectedIds, datasets, setDatasets]); + }, [close, selectedIds, timelineDatasets, setTimelineDatasets]); + + const controlVars = useBrowserControls({ + sortOptions + }); + const { taxonomies, sortField, sortDir, onAction } = controlVars; + const search = controlVars.search ?? ''; + + // Clear filters when the modal is revealed. + useEffect(() => { + if (revealed) { + onAction(Actions.CLEAR); + } + }, [revealed]); + + // Filters are applies to the veda datasets, but then we want to display the + // dataset layers since those are shown on the map. + const displayDatasetLayers = useMemo( + () => + prepareDatasets(allDatasets, { + search, + taxonomies, + sortField, + sortDir + }).flatMap((dataset) => dataset.layers), + [search, taxonomies, sortField, sortDir] + ); + + const isFiltering = !!( + (taxonomies && Object.keys(taxonomies).length) || + search + ); + + const isFirstSelection = timelineDatasets.length === 0; return ( - -
- - {datasetLayers.map((datasetLayer) => ( - onCheck(datasetLayer.id)} - checked={selectedIds.includes(datasetLayer.id)} - > - - From: {findParentDataset(datasetLayer.id)?.name} - - {datasetLayer.name} - - ))} - -
+ + {isFirstSelection ? ( +

Select datasets to start the exploration.

+ ) : ( +

Add or remove datasets to the exploration.

+ )} +
+ + + + Showing{' '} + {' '} + out of {datasetLayers.length}. + + {isFiltering && ( + + )} + + + + {displayDatasetLayers.length ? ( + + {displayDatasetLayers.map((datasetLayer) => { + const parent = findParentDataset(datasetLayer.id); + if (!parent) return null; + + return ( +
  • + onCheck(datasetLayer.id)} + /> +
  • + ); + })} +
    + ) : ( + + There are no datasets to show with the selected filters. + + )} +
    } footerContent={ - + <> +

    + {selectedIds.length + ? `${selectedIds.length} out of ${datasetLayers.length} datasets selected.` + : 'No datasets selected.'} +

    + {!isFirstSelection && ( + + )} + + + } + /> + ); +} + +interface DatasetLayerProps { + parent: DatasetData; + layer: DatasetLayer; + selected: boolean; + onDatasetClick: () => void; +} + +function DatasetLayerCard(props: DatasetLayerProps) { + const { parent, layer, onDatasetClick, selected } = props; + + const topics = getTaxonomy(parent, TAXONOMY_TOPICS)?.values; + + return ( + + + + {/* */} + + } + linkTo={getDatasetPath(parent)} + linkLabel='View dataset' + onLinkClick={(e) => { + e.preventDefault(); + onDatasetClick(); + }} + title={layer.name} + description={`From: ${parent.name}`} + imgSrc={parent.media?.src} + imgAlt={parent.media?.alt} + footerContent={ + <> + {topics?.length ? ( + +
    Topics
    + {topics.map((t) => ( +
    + {t.name} +
    + ))} +
    + ) : null} + + } /> ); diff --git a/app/scripts/components/exploration/data-utils.ts b/app/scripts/components/exploration/data-utils.ts index dd481bd85..57ebd33a2 100644 --- a/app/scripts/components/exploration/data-utils.ts +++ b/app/scripts/components/exploration/data-utils.ts @@ -24,6 +24,8 @@ export const findParentDataset = (layerId: string) => { return parentDataset?.data; }; +export const allDatasets = Object.values(datasets).map((d) => d!.data); + export const datasetLayers = Object.values(datasets).flatMap( (dataset) => dataset!.data.layers ); diff --git a/app/scripts/styles/theme.ts b/app/scripts/styles/theme.ts index f5498e7a4..f33a17aa3 100644 --- a/app/scripts/styles/theme.ts +++ b/app/scripts/styles/theme.ts @@ -10,7 +10,7 @@ export const VEDA_OVERRIDE_THEME = { hide: -1, docked: 10, sticky: 900, - dropdown: 1000, + dropdown: 1550, overlay: 1300, modal: 1400, popover: 1500, From df314d24cf790f615e401bf80bb1bf46a58d07fb Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Tue, 17 Oct 2023 13:08:50 +0100 Subject: [PATCH 5/5] Update modal style --- .../components/dataset-selector-modal.tsx | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/app/scripts/components/exploration/components/dataset-selector-modal.tsx b/app/scripts/components/exploration/components/dataset-selector-modal.tsx index aff8e78f1..e3e3fed32 100644 --- a/app/scripts/components/exploration/components/dataset-selector-modal.tsx +++ b/app/scripts/components/exploration/components/dataset-selector-modal.tsx @@ -3,10 +3,16 @@ import styled, { css } from 'styled-components'; import { useAtom } from 'jotai'; import { DatasetData, DatasetLayer, datasetTaxonomies } from 'veda'; -import { Modal, ModalBody, ModalFooter } from '@devseed-ui/modal'; +import { + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalHeadline +} from '@devseed-ui/modal'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { Button } from '@devseed-ui/button'; -import { Subtitle } from '@devseed-ui/typography'; +import { Heading, Subtitle } from '@devseed-ui/typography'; import { CollecticonTickSmall, CollecticonXmarkSmall, @@ -51,11 +57,18 @@ const DatasetModal = styled(Modal)` /* Override ModalContents */ > div { - height: calc(100vh - ${glsp(4)}); display: flex; flex-flow: column; } + ${ModalHeader} { + position: sticky; + top: ${glsp(-2)}; + z-index: 100; + box-shadow: 0 1px 0 0 ${themeVal('color.base-100a')}; + margin-bottom: ${glsp(2)}; + } + ${ModalBody} { height: 100%; min-height: 0; @@ -68,6 +81,10 @@ const DatasetModal = styled(Modal)` display: flex; gap: ${glsp(1)}; align-items: center; + position: sticky; + bottom: ${glsp(-2)}; + z-index: 100; + box-shadow: 0 -1px 0 0 ${themeVal('color.base-100a')}; > .selection-info { margin-right: auto; @@ -75,9 +92,7 @@ const DatasetModal = styled(Modal)` } `; -const ModalIntro = styled.div` - margin-bottom: ${glsp(2)}; -`; +const ModalIntro = styled.div``; const DatasetCount = styled(Subtitle)` display: flex; @@ -93,13 +108,10 @@ const DatasetContainer = styled.div` height: 100%; min-height: 0; display: flex; - margin: ${glsp(0, -2)}; - box-shadow: 0 -1px 0 0 ${themeVal('color.base-100a')}, - inset 0 -1px 0 0 ${themeVal('color.base-100a')}; + margin-bottom: ${glsp(2)}; ${CardList} { - overflow-y: auto; - padding: ${glsp(2)}; + width: 100%; } ${EmptyHub} { @@ -236,8 +248,9 @@ export function DatasetSelectorModal(props: DatasetSelectorModalProps) { revealed={revealed} closeButton={!isFirstSelection} onCloseClick={close} - content={ - <> + renderHeadline={() => ( + + Select datasets {isFirstSelection ? (

    Select datasets to start the exploration.

    @@ -245,6 +258,10 @@ export function DatasetSelectorModal(props: DatasetSelectorModalProps) {

    Add or remove datasets to the exploration.

    )}
    +
    + )} + content={ + <> - {/* */} } linkTo={getDatasetPath(parent)}