From 2d58b7f80a87eebe21d829163bf4d4bd16139f32 Mon Sep 17 00:00:00 2001 From: Phil Owen <19691521+PhillipsOwen@users.noreply.github.com> Date: Thu, 2 May 2024 12:13:33 -0400 Subject: [PATCH 01/17] updating the mui packages to the latest, adding react-draggable and prop-types. --- package-lock.json | 29 ++++++++++++++++++++++++++--- package.json | 8 +++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 68cd94a2..2621f19f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,19 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/inter": "^5.0.16", - "@mui/icons-material": "^5.15.11", - "@mui/joy": "^5.0.0-beta.32", - "@mui/material": "^5.15.11", + "@mui/icons-material": "^5.15.15", + "@mui/joy": "^5.0.0-beta.36", + "@mui/material": "^5.15.15", "axios": "^1.6.7", "core-js": "^3.36.0", "d3": "^7.8.5", "dotenv": "^16.4.5", "leaflet": "^1.9.4", "mapbox-gl": "^3.1.2", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-leaflet": "^4.2.1", "react-map-gl": "^7.1.7", "react-query": "^3.39.3", @@ -15731,6 +15733,27 @@ "react": "^18.3.1" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/package.json b/package.json index 36884e00..883d0464 100644 --- a/package.json +++ b/package.json @@ -48,17 +48,19 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/inter": "^5.0.16", - "@mui/icons-material": "^5.15.11", - "@mui/joy": "^5.0.0-beta.32", - "@mui/material": "^5.15.11", + "@mui/icons-material": "^5.15.15", + "@mui/joy": "^5.0.0-beta.36", + "@mui/material": "^5.15.15", "axios": "^1.6.7", "core-js": "^3.36.0", "d3": "^7.8.5", "dotenv": "^16.4.5", "leaflet": "^1.9.4", "mapbox-gl": "^3.1.2", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-leaflet": "^4.2.1", "react-map-gl": "^7.1.7", "react-query": "^3.39.3", From 463cd6b7e711588d38b0deab8e0b2f2ea9826739 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Thu, 2 May 2024 23:43:02 -0400 Subject: [PATCH 02/17] create layer actions add up/down buttons to layer cards add adjacent swap ordering buttons to layers add layer move and delete actions --- src/components/map/default-layers.js | 13 +- src/components/sidebar/tray.js | 4 +- src/components/trays/layers/list.js | 200 +++++++++++++++------------ src/context/map-context.js | 41 +++++- 4 files changed, 159 insertions(+), 99 deletions(-) diff --git a/src/components/map/default-layers.js b/src/components/map/default-layers.js index 0b3b9f0d..9204cb1b 100644 --- a/src/components/map/default-layers.js +++ b/src/components/map/default-layers.js @@ -4,17 +4,19 @@ import { CircleMarker } from 'leaflet'; import { useLayers } from '@context'; import { markClicked } from '@utils/map-utils'; -const newLayerDefaultState = layer => { +const newLayerDefaultState = (layer, order) => { const { product_type } = layer.properties; if (['obs', 'maxele63'].includes(product_type)) { return ({ - visible: true + order, + visible: true, }); } return ({ - visible: false + order, + visible: false, }); }; @@ -111,7 +113,7 @@ export const DefaultLayers = () => { if (layer) layer_list.push({ ...layer, - state: newLayerDefaultState(layer) + state: newLayerDefaultState(layer, layer_list.length) }); // TODO: do we really need to do this here??! @@ -160,7 +162,8 @@ export const DefaultLayers = () => { return ( <> {defaultModelLayers - .filter(({state }) => state.visible) + .filter(({ state }) => state.visible) + .sort((a, b) => a.state.order - b.state.order) .map((layer, index) => { const pieces = layer.id.split('-'); const type = pieces[pieces.length-1]; diff --git a/src/components/sidebar/tray.js b/src/components/sidebar/tray.js index 84916848..1b9a7467 100644 --- a/src/components/sidebar/tray.js +++ b/src/components/sidebar/tray.js @@ -12,6 +12,8 @@ import { KeyboardDoubleArrowLeft as CloseTrayIcon, } from '@mui/icons-material'; +const TRAY_WIDTH = '500px'; + export const Tray = ({ active, Contents, title, closeHandler }) => { return ( { transform: active ? 'translateX(0)' : 'translateX(-120%)', transition: 'transform 250ms', height: '100vh', - width: '450px', + width: TRAY_WIDTH, zIndex: 998, filter: 'drop-shadow(0 0 8px rgba(0, 0, 0, 0.2))', overflowX: 'hidden', diff --git a/src/components/trays/layers/list.js b/src/components/trays/layers/list.js index c02144df..14df997e 100644 --- a/src/components/trays/layers/list.js +++ b/src/components/trays/layers/list.js @@ -4,6 +4,7 @@ import { AccordionGroup, AccordionDetails, Box, + ButtonGroup, Divider, IconButton, ListItemContent, @@ -12,13 +13,20 @@ import { Typography, } from '@mui/joy'; import { - DragIndicator as DragHandleIcon, + DeleteForever as RemoveIcon, KeyboardArrowDown as ExpandIcon, + ArrowDropUp as MoveUpArrow, + ArrowDropDown as MoveDownArrow, } from '@mui/icons-material'; import { useLayers } from '@context'; export const LayersList = () => { - const { defaultModelLayers, toggleLayerVisibility } = useLayers(); + const { + defaultModelLayers, + removeLayer, + swapLayers, + toggleLayerVisibility, + } = useLayers(); const layers = [...defaultModelLayers]; // convert the product type to a readable layer name @@ -31,8 +39,6 @@ export const LayersList = () => { hec_ras_water_surface: "HEC/RAS Water Surface", }; - console.table(layers[0]); - const [expandedIds, setExpandedIds] = useState(new Set()); const handleToggleExpansion = id => () => { @@ -46,106 +52,118 @@ export const LayersList = () => { setExpandedIds(_expandedIds); }; + const handleClickRemove = id => () => { + removeLayer(id) + } + return ( { - layers.map(layer => { - const isExpanded = expandedIds.has(layer.id); - const isVisible = layer.state.visible; - const layerTitle = layer_names[layer.properties.product_type] + " " + - layer.properties.run_date + " " + - layer.properties.cycle; + layers + .sort((a, b) => a.state.order - b.state.order) + .map(layer => { + const isExpanded = expandedIds.has(layer.id); + const isVisible = layer.state.visible; + const layerTitle = layer_names[layer.properties.product_type] + " " + + layer.properties.run_date + " " + + layer.properties.cycle; - return ( - - {/* - the usual AccordionSummary component results in a button, - but we want some buttons _inside_ the accordion summary, - so we'll build a custom component here. - */} - - - - + + swapLayers(layer.state.order, layer.state.order - 1) } + > + swapLayers(layer.state.order, layer.state.order + 1) } + > + + + + + {layerTitle} + + + { layer.layers } + + - - - {layerTitle} - - - { layer.layers } - - + + toggleLayerVisibility(layer.id) } + /> - toggleLayerVisibility(layer.id) } - /> + + + + - - - - - - + + + + - { JSON.stringify(layer.properties, null, 2) } - - - - ); - }) + + { JSON.stringify(layer.properties, null, 2) } + + + + ); + }) } diff --git a/src/context/map-context.js b/src/context/map-context.js index c2632def..f55ecf65 100644 --- a/src/context/map-context.js +++ b/src/context/map-context.js @@ -17,7 +17,7 @@ export const LayersProvider = ({ children }) => { const newLayers = [...defaultModelLayers]; const index = newLayers.findIndex(l => l.id === id); if (index === -1) { - new Error('Could not locate layer', id); + console.error('Could not locate layer', id); return; } const alteredLayer = newLayers[index]; @@ -29,6 +29,41 @@ export const LayersProvider = ({ children }) => { ]); }; + const swapLayers = (i, j) => { + const ithLayerIndex = defaultModelLayers + .findIndex(({ state }) => state.order === i); + const jthLayerIndex = defaultModelLayers + .findIndex(({ state }) => state.order === j); + if (ithLayerIndex === -1 || jthLayerIndex === -1) { + return defaultModelLayers; + } + const newLayers = [...defaultModelLayers] ; + newLayers[ithLayerIndex].state.order = j; + newLayers[jthLayerIndex].state.order = i; + setDefaultModelLayers(newLayers); + }; + + const removeLayer = id => { + const index = defaultModelLayers.findIndex(l => l.id === id) + if (index === -1) { + return + } + const thisPosition = defaultModelLayers[index].state.order + const newLayers = [...defaultModelLayers.slice(0, index), ...defaultModelLayers.slice(index + 1)] + .map(l => { + if (l.state.order > thisPosition) { + l.state.order -= 1; + } + return l + }); + + setDefaultModelLayers(newLayers) + /* todo: update `layer.state.order`s + layer.state.order - 1 for all layers l with l.state.order > this layer's state.order + */ + }; + + return ( { setFilteredModelLayers, toggleLayerVisibility, selectedObservations, - setSelectedObservations + setSelectedObservations, + swapLayers, + removeLayer }} > {children} From a7c3c620900940d5b6195a8c3eb44b1cacaa8dd3 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sat, 4 May 2024 09:55:11 -0400 Subject: [PATCH 03/17] use reduce in delete logic --- src/context/map-context.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/context/map-context.js b/src/context/map-context.js index f55ecf65..211bd0ab 100644 --- a/src/context/map-context.js +++ b/src/context/map-context.js @@ -49,13 +49,16 @@ export const LayersProvider = ({ children }) => { return } const thisPosition = defaultModelLayers[index].state.order - const newLayers = [...defaultModelLayers.slice(0, index), ...defaultModelLayers.slice(index + 1)] - .map(l => { - if (l.state.order > thisPosition) { - l.state.order -= 1; - } - return l - }); + const newLayers = defaultModelLayers.reduce((acc, l) => { + if (l.state.order === thisPosition) { + return acc + } + if (l.state.order > thisPosition) { + l.state.order -= 1; + } + acc.push(l) + return acc + }, []) setDefaultModelLayers(newLayers) /* todo: update `layer.state.order`s From ef593b65dff0cea537a220b034d98a476bfa6af7 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sat, 4 May 2024 10:06:49 -0400 Subject: [PATCH 04/17] lint fix --- src/context/map-context.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/context/map-context.js b/src/context/map-context.js index 211bd0ab..f1814f91 100644 --- a/src/context/map-context.js +++ b/src/context/map-context.js @@ -44,23 +44,23 @@ export const LayersProvider = ({ children }) => { }; const removeLayer = id => { - const index = defaultModelLayers.findIndex(l => l.id === id) + const index = defaultModelLayers.findIndex(l => l.id === id); if (index === -1) { - return + return; } - const thisPosition = defaultModelLayers[index].state.order + const thisPosition = defaultModelLayers[index].state.order; const newLayers = defaultModelLayers.reduce((acc, l) => { if (l.state.order === thisPosition) { - return acc + return acc; } if (l.state.order > thisPosition) { l.state.order -= 1; } - acc.push(l) - return acc - }, []) + acc.push(l); + return acc; + }, []); - setDefaultModelLayers(newLayers) + setDefaultModelLayers(newLayers); /* todo: update `layer.state.order`s layer.state.order - 1 for all layers l with l.state.order > this layer's state.order */ From 3cd34d74d4d9fcd967e05631ffba7da32b790771 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sat, 4 May 2024 12:22:55 -0400 Subject: [PATCH 05/17] tidy up layer card contents use small expand button --- src/components/trays/layers/list.js | 98 ++++++++++++++++------------- src/context/map-context.js | 39 +++++++++++- 2 files changed, 92 insertions(+), 45 deletions(-) diff --git a/src/components/trays/layers/list.js b/src/components/trays/layers/list.js index 14df997e..fc578458 100644 --- a/src/components/trays/layers/list.js +++ b/src/components/trays/layers/list.js @@ -3,11 +3,11 @@ import { Accordion, AccordionGroup, AccordionDetails, + Avatar, Box, ButtonGroup, Divider, IconButton, - ListItemContent, Stack, Switch, Typography, @@ -17,28 +17,21 @@ import { KeyboardArrowDown as ExpandIcon, ArrowDropUp as MoveUpArrow, ArrowDropDown as MoveDownArrow, + Schedule as ClockIcon, + DragIndicator as DragHandleIcon, } from '@mui/icons-material'; import { useLayers } from '@context'; export const LayersList = () => { const { defaultModelLayers, + layerTypes, removeLayer, swapLayers, toggleLayerVisibility, } = useLayers(); const layers = [...defaultModelLayers]; - // convert the product type to a readable layer name - const layer_names = { - obs: "Observations", - maxwvel63: "Maximum Wind Velocity", - maxele63: "Maximum Water Level", - swan_HS_max63: "Maximum Wave Height", - maxele_level_downscaled_epsg4326: "Hi-Res Maximum Water Level", - hec_ras_water_surface: "HEC/RAS Water Surface", - }; - const [expandedIds, setExpandedIds] = useState(new Set()); const handleToggleExpansion = id => () => { @@ -53,8 +46,8 @@ export const LayersList = () => { }; const handleClickRemove = id => () => { - removeLayer(id) - } + removeLayer(id); + }; return ( @@ -64,10 +57,7 @@ export const LayersList = () => { .map(layer => { const isExpanded = expandedIds.has(layer.id); const isVisible = layer.state.visible; - const layerTitle = layer_names[layer.properties.product_type] + " " + - layer.properties.run_date + " " + - layer.properties.cycle; - + const LayerIcon = layerTypes[layer.properties.product_type].icon; return ( { borderLeft: '6px solid', borderLeftColor: isVisible ? 'primary.400' : 'primary.100', '.MuiIconButton-root': { filter: 'opacity(0.1)', transition: 'filter 250ms' }, - '.MuiSwitch-root': { filter: 'opacity(0.1)', transition: 'filter 250ms' }, + '.MuiSwitch-root': { + filter: 'opacity(0.1)', + transition: 'filter 250ms', + }, '&:hover .MuiIconButton-root': { filter: 'opacity(0.5)' }, '&:hover .MuiSwitch-root': { filter: 'opacity(0.5)' }, '& .MuiIconButton-root:hover': { filter: 'opacity(1.0)' }, '& .MuiSwitch-root:hover': { filter: 'opacity(1.0)' }, }} > - - swapLayers(layer.state.order, layer.state.order - 1) } - > - swapLayers(layer.state.order, layer.state.order + 1) } - > - - - - - {layerTitle} - - - { layer.layers } - - + + + + + { console.log(layer)} + + {layerTypes[layer.properties.product_type].name} + + + + + + + + + { new Date(layer.properties.run_date).toLocaleString() } + + Cycle { layer.properties.cycle } + + + + { - + + swapLayers(layer.state.order, layer.state.order - 1) } + > + swapLayers(layer.state.order, layer.state.order + 1) } + > + + + useContext(LayersContext); +// convert the product type to a readable layer name +const layerTypes = { + obs: { + name: "Observations", + icon: ObservationIcon, + }, + maxwvel63: { + name: "Maximum Wind Velocity", + icon: WindVelocityIcon, + }, + maxele63: { + name: "Maximum Water Level", + icon: WaterLevelIcon, + }, + swan_HS_max63: { + name: "Maximum Wave Height", + icon: WaveHeightIcon, + }, + maxele_level_downscaled_epsg4326: { + name: "Hi-Res Maximum Water Level", + icon: WaterLevelIcon, + }, + hec_ras_water_surface: { + name: "HEC/RAS Water Surface", + icon: WaterSurfaceIcon, + }, +}; + export const LayersProvider = ({ children }) => { const [defaultModelLayers, setDefaultModelLayers] = useState([]); const [filteredModelLayers, setFilteredModelLayers] = useState([]); @@ -80,7 +116,8 @@ export const LayersProvider = ({ children }) => { selectedObservations, setSelectedObservations, swapLayers, - removeLayer + removeLayer, + layerTypes, }} > {children} From 9f9993d0f1ca7e080c9e50b42ba12a61c7cd54bc Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sat, 4 May 2024 12:47:19 -0400 Subject: [PATCH 06/17] minor styling fix move button flex minor styling remove comment --- src/components/trays/layers/list.js | 30 +++++++++++++++++------------ src/context/map-context.js | 3 --- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/trays/layers/list.js b/src/components/trays/layers/list.js index fc578458..47279ca5 100644 --- a/src/components/trays/layers/list.js +++ b/src/components/trays/layers/list.js @@ -78,15 +78,9 @@ export const LayersList = () => { p: 1, borderLeft: '6px solid', borderLeftColor: isVisible ? 'primary.400' : 'primary.100', - '.MuiIconButton-root': { filter: 'opacity(0.1)', transition: 'filter 250ms' }, - '.MuiSwitch-root': { - filter: 'opacity(0.1)', - transition: 'filter 250ms', - }, - '&:hover .MuiIconButton-root': { filter: 'opacity(0.5)' }, - '&:hover .MuiSwitch-root': { filter: 'opacity(0.5)' }, - '& .MuiIconButton-root:hover': { filter: 'opacity(1.0)' }, - '& .MuiSwitch-root:hover': { filter: 'opacity(1.0)' }, + '.actions': { filter: 'opacity(0.1)', transition: 'filter 250ms' }, + '&:hover .actions': { filter: 'opacity(0.5)' }, + '& .actions:hover': { filter: 'opacity(1.0)' }, }} > @@ -123,7 +117,8 @@ export const LayersList = () => { - + {/* card actions start */} + { - + swapLayers(layer.state.order, layer.state.order - 1) } + sx={{ flex: 1 }} > swapLayers(layer.state.order, layer.state.order + 1) } + sx={{ flex: 1 }} > + {/* card actions end */} - + { }, []); setDefaultModelLayers(newLayers); - /* todo: update `layer.state.order`s - layer.state.order - 1 for all layers l with l.state.order > this layer's state.order - */ }; From f5c7f878261a5eb1b67b114a345c4fcab9cf70ab Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sat, 4 May 2024 13:35:04 -0400 Subject: [PATCH 07/17] ordering on the layers array itself --- src/components/map/default-layers.js | 5 +---- src/components/sidebar/sidebar.js | 2 +- src/components/trays/layers/list.js | 16 ++++++++++------ src/context/map-context.js | 28 +++++++++++++++++----------- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/components/map/default-layers.js b/src/components/map/default-layers.js index 9204cb1b..6f655511 100644 --- a/src/components/map/default-layers.js +++ b/src/components/map/default-layers.js @@ -4,18 +4,16 @@ import { CircleMarker } from 'leaflet'; import { useLayers } from '@context'; import { markClicked } from '@utils/map-utils'; -const newLayerDefaultState = (layer, order) => { +const newLayerDefaultState = (layer) => { const { product_type } = layer.properties; if (['obs', 'maxele63'].includes(product_type)) { return ({ - order, visible: true, }); } return ({ - order, visible: false, }); }; @@ -163,7 +161,6 @@ export const DefaultLayers = () => { <> {defaultModelLayers .filter(({ state }) => state.visible) - .sort((a, b) => a.state.order - b.state.order) .map((layer, index) => { const pieces = layer.id.split('-'); const type = pieces[pieces.length-1]; diff --git a/src/components/sidebar/sidebar.js b/src/components/sidebar/sidebar.js index 20ecc19e..ddc8a92b 100644 --- a/src/components/sidebar/sidebar.js +++ b/src/components/sidebar/sidebar.js @@ -9,7 +9,7 @@ import { MenuItem } from './menu-item'; import SidebarTrays from '../trays'; export const Sidebar = () => { - const [activeIndex, setActiveIndex] = useState(-1); + const [activeIndex, setActiveIndex] = useState(0); const handleClickMenuItem = useCallback(newIndex => { // if the incoming new index equals the old index, diff --git a/src/components/trays/layers/list.js b/src/components/trays/layers/list.js index 47279ca5..0c49bd7b 100644 --- a/src/components/trays/layers/list.js +++ b/src/components/trays/layers/list.js @@ -54,7 +54,7 @@ export const LayersList = () => { { layers .sort((a, b) => a.state.order - b.state.order) - .map(layer => { + .map((layer, index) => { const isExpanded = expandedIds.has(layer.id); const isVisible = layer.state.visible; const LayerIcon = layerTypes[layer.properties.product_type].icon; @@ -84,10 +84,14 @@ export const LayersList = () => { }} > - + - - { console.log(layer)} + + {layerTypes[layer.properties.product_type].name} @@ -144,11 +148,11 @@ export const LayersList = () => { }} > swapLayers(layer.state.order, layer.state.order - 1) } + onClick={ () => swapLayers(index, index - 1) } sx={{ flex: 1 }} > swapLayers(layer.state.order, layer.state.order + 1) } + onClick={ () => swapLayers(index, index + 1) } sx={{ flex: 1 }} > diff --git a/src/context/map-context.js b/src/context/map-context.js index dcb32910..41e9d93f 100644 --- a/src/context/map-context.js +++ b/src/context/map-context.js @@ -66,17 +66,23 @@ export const LayersProvider = ({ children }) => { }; const swapLayers = (i, j) => { - const ithLayerIndex = defaultModelLayers - .findIndex(({ state }) => state.order === i); - const jthLayerIndex = defaultModelLayers - .findIndex(({ state }) => state.order === j); - if (ithLayerIndex === -1 || jthLayerIndex === -1) { - return defaultModelLayers; - } - const newLayers = [...defaultModelLayers] ; - newLayers[ithLayerIndex].state.order = j; - newLayers[jthLayerIndex].state.order = i; - setDefaultModelLayers(newLayers); + // ensure our pair has i < j + const [a, b] = [i, j].sort() + // bail out for select (a, b) pairs. + if ( + a === b || a < 0 || b < 0 + || defaultModelLayers.length - 2 < a + || defaultModelLayers.length - 1 < b + ) { return } + + const newLayers = [ + ...defaultModelLayers.slice(0, a), + defaultModelLayers[b], + ...defaultModelLayers.slice(a + 1, b), + defaultModelLayers[a], + ...defaultModelLayers.slice(b + 1), + ]; + setDefaultModelLayers([...newLayers]) }; const removeLayer = id => { From bfae93be4017161f0c7b4c8dd67df4050c4b96a6 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sat, 4 May 2024 15:16:41 -0400 Subject: [PATCH 08/17] styling, update delete func --- src/components/map/default-layers.js | 86 ++++++++++++---------------- src/context/map-context.js | 15 +---- 2 files changed, 38 insertions(+), 63 deletions(-) diff --git a/src/components/map/default-layers.js b/src/components/map/default-layers.js index 6f655511..dec98365 100644 --- a/src/components/map/default-layers.js +++ b/src/components/map/default-layers.js @@ -54,11 +54,11 @@ export const DefaultLayers = () => { } return new CircleMarker(latlng, { - radius: 6, - weight: 0.7, - color: '#000000', - fillColor: obs_color, - fillOpacity: 1 + radius: 6, + weight: 0.7, + color: '#000000', + fillColor: obs_color, + fillOpacity: 1 }); }); @@ -153,49 +153,35 @@ export const DefaultLayers = () => { return entry; }; getDefaultLayers().then(); - }, []); - - //console.log("defaultModelLayers: " + JSON.stringify(defaultModelLayers, null, 2)) - - return ( - <> - {defaultModelLayers - .filter(({ state }) => state.visible) - .map((layer, index) => { - const pieces = layer.id.split('-'); - const type = pieces[pieces.length-1]; - //console.log("type: " + JSON.stringify(type, null, 2)) - if( type === "obs" && obsData !== "") { - //console.log("obsData: " + JSON.stringify(obsData, null, 2)); - return ( - - ); - } else { - return ( - { - console.log('marker clicked') - }, - }} */ - url={gs_wms_url} - layers={layer.layers} - params={{ - format:"image/png", - transparent: true, - }} - - /> - ); - } - }) - }; - - ); + }, []); + + return defaultModelLayers + .filter(({ state }) => state.visible) + .reverse() + .map((layer, index) => { + const pieces = layer.id.split('-'); + const type = pieces[pieces.length-1]; + if (type === "obs" && obsData !== "") { + return ( + + ); + } + return ( + + ); + }); }; \ No newline at end of file diff --git a/src/context/map-context.js b/src/context/map-context.js index 41e9d93f..11c24fe7 100644 --- a/src/context/map-context.js +++ b/src/context/map-context.js @@ -74,7 +74,7 @@ export const LayersProvider = ({ children }) => { || defaultModelLayers.length - 2 < a || defaultModelLayers.length - 1 < b ) { return } - + const newLayers = [ ...defaultModelLayers.slice(0, a), defaultModelLayers[b], @@ -90,18 +90,7 @@ export const LayersProvider = ({ children }) => { if (index === -1) { return; } - const thisPosition = defaultModelLayers[index].state.order; - const newLayers = defaultModelLayers.reduce((acc, l) => { - if (l.state.order === thisPosition) { - return acc; - } - if (l.state.order > thisPosition) { - l.state.order -= 1; - } - acc.push(l); - return acc; - }, []); - + const newLayers = defaultModelLayers.filter(l => l.id !== id); setDefaultModelLayers(newLayers); }; From 50c925317d92790c9c5ada1a35a5f03223298aa8 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sat, 4 May 2024 15:47:59 -0400 Subject: [PATCH 09/17] rerender layers when properties change align inconsistent indentation --- src/components/map/default-layers.js | 323 +++++++++++++-------------- src/context/map-context.js | 19 +- 2 files changed, 169 insertions(+), 173 deletions(-) diff --git a/src/components/map/default-layers.js b/src/components/map/default-layers.js index dec98365..6f855965 100644 --- a/src/components/map/default-layers.js +++ b/src/components/map/default-layers.js @@ -1,187 +1,186 @@ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { WMSTileLayer, GeoJSON, useMap } from 'react-leaflet'; import { CircleMarker } from 'leaflet'; import { useLayers } from '@context'; import { markClicked } from '@utils/map-utils'; const newLayerDefaultState = (layer) => { - const { product_type } = layer.properties; - - if (['obs', 'maxele63'].includes(product_type)) { - return ({ - visible: true, - }); - } + const { product_type } = layer.properties; + if (['obs', 'maxele63'].includes(product_type)) { return ({ - visible: false, + visible: true, }); + } + + return ({ + visible: false, + }); }; export const DefaultLayers = () => { - const [obsData, setObsData] = useState(""); - const map = useMap(); + const [obsData, setObsData] = useState(""); + const map = useMap(); - const { - defaultModelLayers, - setDefaultModelLayers, - setSelectedObservations - } = useLayers(); + const { + defaultModelLayers, + setDefaultModelLayers, + setSelectedObservations + } = useLayers(); - // Create the authorization header - const requestOptions = { - method: 'GET', - headers: { - Authorization: `Bearer ${process.env.REACT_APP_UI_DATA_TOKEN}` - } - }; - - const obsPointToLayer = ((feature, latlng) => { - let obs_color = "#FFFFFF"; - - switch (feature.properties.gauge_owner) { - case 'NOAA/NDBC': - obs_color = "#FFFF00"; - break; - case 'NCEM': - obs_color = "#3D4849"; - break; - case 'NOAA/NOS': - obs_color = "#BEAEFA"; - break; - } - - return new CircleMarker(latlng, { - radius: 6, - weight: 0.7, - color: '#000000', - fillColor: obs_color, - fillOpacity: 1 - }); - }); + // Create the authorization header + const requestOptions = { + method: 'GET', + headers: { + Authorization: `Bearer ${process.env.REACT_APP_UI_DATA_TOKEN}` + } + }; - const onEachObsFeature = (feature, layer) => { - if (feature.properties && feature.properties.location_name) { - const popupContent = feature.properties.location_name; + const obsPointToLayer = ((feature, latlng) => { + let obs_color = "#FFFFFF"; - layer.on("mouseover", function (e) { - this.bindPopup(popupContent).openPopup(e.latlng); - }); - - layer.on("mousemove", function (e) { - this.getPopup().setLatLng(e.latlng); - }); + switch (feature.properties.gauge_owner) { + case 'NOAA/NDBC': + obs_color = "#FFFF00"; + break; + case 'NCEM': + obs_color = "#3D4849"; + break; + case 'NOAA/NOS': + obs_color = "#BEAEFA"; + break; + } - layer.on("mouseout", function () { - this.closePopup(); - }); - layer.on("click", function (e) { - // Do stuff here for retrieving time series data, in csv fomat, - // from the feature.properties.csv_url and create a fancy plot - console.log("Observation Station '" + feature.properties.location_name + "' clicked"); - markClicked(map, e); - - // populate selectedObservations list with the newly selected observation point - setSelectedObservations(previous => [...previous, feature.properties]); + return new CircleMarker(latlng, { + radius: 6, + weight: 0.7, + color: '#000000', + fillColor: obs_color, + fillOpacity: 1 + }); + }); + + const onEachObsFeature = (feature, layer) => { + if (feature.properties && feature.properties.location_name) { + const popupContent = feature.properties.location_name; + + layer.on("mouseover", function (e) { + this.bindPopup(popupContent).openPopup(e.latlng); + }); + + layer.on("mousemove", function (e) { + this.getPopup().setLatLng(e.latlng); + }); + + layer.on("mouseout", function () { + this.closePopup(); + }); + layer.on("click", function (e) { + // Do stuff here for retrieving time series data, in csv fomat, + // from the feature.properties.csv_url and create a fancy plot + console.log("Observation Station '" + feature.properties.location_name + "' clicked"); + markClicked(map, e); + + // populate selectedObservations list with the newly selected observation point + setSelectedObservations(previous => [...previous, feature.properties]); + }); + } + }; + + // create the URLs to the data endpoints + const data_url = `${process.env.REACT_APP_UI_DATA_URL}get_ui_data_secure?limit=1&use_new_wb=true&use_v3_sp=true`; + const gs_wms_url = `${process.env.REACT_APP_GS_DATA_URL}wms`; + const gs_wfs_url = `${process.env.REACT_APP_GS_DATA_URL}`; + + useEffect(() => { + // React advises to declare the async function directly inside useEffect + // TODO: Need to store this url in some website config file and + // it should change to reflect the namspace we are running in + async function getDefaultLayers() { + const layer_list = []; + const response = await fetch(data_url, requestOptions); + const data = await response.json(); + let obs_url = null; + + if (data) { + // get layer id in workbench and find catalog entries for each + data.workbench.forEach(function (layer_id) { + const layer = getCatalogEntry(data.catalog, layer_id); + if (layer) + layer_list.push({ + ...layer, + state: newLayerDefaultState(layer, layer_list.length) }); - } - }; - // create the URLs to the data endpoints - const data_url = `${process.env.REACT_APP_UI_DATA_URL}get_ui_data_secure?limit=1&use_new_wb=true&use_v3_sp=true`; - const gs_wms_url = `${process.env.REACT_APP_GS_DATA_URL}wms`; - const gs_wfs_url = `${process.env.REACT_APP_GS_DATA_URL}`; - - useEffect(() => { - // React advises to declare the async function directly inside useEffect - // TODO: Need to store this url in some website config file and - // it should change to reflect the namspace we are running in - async function getDefaultLayers() { - const layer_list = []; - const response = await fetch(data_url, requestOptions); - const data = await response.json(); - let obs_url = null; - - if (data) { - // get layer id in workbench and find catalog entries for each - data.workbench.forEach(function (layer_id) { - const layer = getCatalogEntry(data.catalog, layer_id); - if (layer) - layer_list.push({ - ...layer, - state: newLayerDefaultState(layer, layer_list.length) - }); - - // TODO: do we really need to do this here??! - // if this is an obs layer, need to retrieve - // the json data for it from GeoServer - const pieces = layer.id.split('-'); - const type = pieces[pieces.length-1]; - if( type === "obs") { - obs_url = gs_wfs_url + - "/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json" + - "&typeName=" + - layer.layers; - } - }); - setDefaultModelLayers(layer_list); - } - - if (obs_url) { - const obs_response = await fetch(obs_url); - const obs_data = await obs_response.json(); - //console.log("obs_data 1: " + JSON.stringify(obs_data, null, 2)) - - setObsData(obs_data); - } + // TODO: do we really need to do this here??! + // if this is an obs layer, need to retrieve + // the json data for it from GeoServer + const pieces = layer.id.split('-'); + const type = pieces[pieces.length-1]; + if( type === "obs") { + obs_url = gs_wfs_url + + "/ows?service=WFS&version=1.0.0&request=GetFeature&outputFormat=application/json" + + "&typeName=" + + layer.layers; + } + }); + setDefaultModelLayers(layer_list); + } - } + if (obs_url) { + const obs_response = await fetch(obs_url); + const obs_data = await obs_response.json(); + //console.log("obs_data 1: " + JSON.stringify(obs_data, null, 2)) - // retrieve the catalog member with the provided id - const getCatalogEntry = (catalog, id) => { - let entry = ""; - - for (const idx in catalog) { - catalog[idx].members.forEach (function (e) { - if (e.id === id) { - entry = e; - } - }); - } - return entry; - }; - getDefaultLayers().then(); - }, []); - - return defaultModelLayers - .filter(({ state }) => state.visible) - .reverse() - .map((layer, index) => { - const pieces = layer.id.split('-'); - const type = pieces[pieces.length-1]; - if (type === "obs" && obsData !== "") { - return ( - - ); - } - return ( - - ); + setObsData(obs_data); + } + + } + + // retrieve the catalog member with the provided id + const getCatalogEntry = (catalog, id) => { + let entry = ""; + + for (const idx in catalog) { + catalog[idx].members.forEach (function (e) { + if (e.id === id) { + entry = e; + } }); + } + return entry; + }; + getDefaultLayers().then(); + }, []); + + return defaultModelLayers + .filter(({ state }) => state.visible) + .reverse() + .map((layer, index) => { + const pieces = layer.id.split('-'); + const type = pieces[pieces.length-1]; + if (type === "obs" && obsData !== "") { + return ( + + ); + } + return ( + + ); + }); }; \ No newline at end of file diff --git a/src/context/map-context.js b/src/context/map-context.js index 11c24fe7..46b4cd21 100644 --- a/src/context/map-context.js +++ b/src/context/map-context.js @@ -67,22 +67,19 @@ export const LayersProvider = ({ children }) => { const swapLayers = (i, j) => { // ensure our pair has i < j - const [a, b] = [i, j].sort() + const [a, b] = [i, j].sort(); // bail out for select (a, b) pairs. if ( - a === b || a < 0 || b < 0 + a === b || a < 0 || b < 1 || defaultModelLayers.length - 2 < a || defaultModelLayers.length - 1 < b - ) { return } + ) { return; } - const newLayers = [ - ...defaultModelLayers.slice(0, a), - defaultModelLayers[b], - ...defaultModelLayers.slice(a + 1, b), - defaultModelLayers[a], - ...defaultModelLayers.slice(b + 1), - ]; - setDefaultModelLayers([...newLayers]) + const newLayers = [...defaultModelLayers]; + const temp = newLayers[i]; + newLayers[i] = newLayers[j]; + newLayers[j] = temp; + setDefaultModelLayers(newLayers); }; const removeLayer = id => { From 722f681c670bfb8aa280df69e5d3d285ab36fbe0 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sat, 4 May 2024 17:25:17 -0400 Subject: [PATCH 10/17] break layer card into separate component we don't keep expanded cards in state any longer. --- src/components/trays/layers/list.js | 316 +++++++++++++++------------- 1 file changed, 168 insertions(+), 148 deletions(-) diff --git a/src/components/trays/layers/list.js b/src/components/trays/layers/list.js index 0c49bd7b..bcd48f58 100644 --- a/src/components/trays/layers/list.js +++ b/src/components/trays/layers/list.js @@ -1,4 +1,5 @@ -import React, { useState } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; import { Accordion, AccordionGroup, @@ -19,173 +20,192 @@ import { ArrowDropDown as MoveDownArrow, Schedule as ClockIcon, DragIndicator as DragHandleIcon, + Opacity as OpacityIcon, + Palette as ColorRampIcon, } from '@mui/icons-material'; import { useLayers } from '@context'; +import { useToggleState } from '@hooks'; export const LayersList = () => { + const { defaultModelLayers } = useLayers(); + const layers = [...defaultModelLayers]; + + return ( + + { + layers + .sort((a, b) => a.state.order - b.state.order) + .map((layer, index) => { + return ( + + ); + }) + } + + + ); +}; + +const LayerCard = ({ index, layer }) => { const { - defaultModelLayers, layerTypes, removeLayer, swapLayers, toggleLayerVisibility, } = useLayers(); - const layers = [...defaultModelLayers]; - - const [expandedIds, setExpandedIds] = useState(new Set()); - - const handleToggleExpansion = id => () => { - const _expandedIds = new Set([...expandedIds]); - if (_expandedIds.has(id)) { - _expandedIds.delete(id); - setExpandedIds(_expandedIds); - return; - } - _expandedIds.add(id); - setExpandedIds(_expandedIds); - }; + const expanded = useToggleState(false); + const isVisible = layer.state.visible; + const LayerIcon = layerTypes[layer.properties.product_type].icon; const handleClickRemove = id => () => { removeLayer(id); }; return ( - - { - layers - .sort((a, b) => a.state.order - b.state.order) - .map((layer, index) => { - const isExpanded = expandedIds.has(layer.id); - const isVisible = layer.state.visible; - const LayerIcon = layerTypes[layer.properties.product_type].icon; + + {/* + the usual AccordionSummary component results in a button, + but we want some buttons _inside_ the accordion summary, + so we'll build a custom component here. + */} + + + + + + + + {layerTypes[layer.properties.product_type].name} + + - return ( - - {/* - the usual AccordionSummary component results in a button, - but we want some buttons _inside_ the accordion summary, - so we'll build a custom component here. - */} - - - - - - - - {layerTypes[layer.properties.product_type].name} - - + + + + + + { new Date(layer.properties.run_date).toLocaleString() } + + Cycle { layer.properties.cycle } + + + + - - - - - - { new Date(layer.properties.run_date).toLocaleString() } - - Cycle { layer.properties.cycle } - - - - + {/* layer card actions start */} + + toggleLayerVisibility(layer.id) } + /> - {/* card actions start */} - - toggleLayerVisibility(layer.id) } - /> + + + + - - - - + + + + + + + + + + + swapLayers(index, index - 1) } + sx={{ flex: 1 }} + > + swapLayers(index, index + 1) } + sx={{ flex: 1 }} + > + + + {/* layer card actions end */} + + + + + + + + { JSON.stringify(layer.properties, null, 2) } + + + - - swapLayers(index, index - 1) } - sx={{ flex: 1 }} - > - swapLayers(index, index + 1) } - sx={{ flex: 1 }} - > - - {/* card actions end */} - - - - - - - - { JSON.stringify(layer.properties, null, 2) } - - - - ); - }) - } - - ); }; + +LayerCard.propTypes = { + index: PropTypes.number.isRequired, + layer: PropTypes.object.isRequired, +}; From fca658b7cf6a1e3971840842c99916ba4f527cd7 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sun, 5 May 2024 14:31:15 -0400 Subject: [PATCH 11/17] break actions into separate component cleanup cleanup --- src/components/trays/layers/list.js | 113 ++++++++++++++-------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/src/components/trays/layers/list.js b/src/components/trays/layers/list.js index bcd48f58..d7782f9f 100644 --- a/src/components/trays/layers/list.js +++ b/src/components/trays/layers/list.js @@ -19,7 +19,6 @@ import { ArrowDropUp as MoveUpArrow, ArrowDropDown as MoveDownArrow, Schedule as ClockIcon, - DragIndicator as DragHandleIcon, Opacity as OpacityIcon, Palette as ColorRampIcon, } from '@mui/icons-material'; @@ -53,7 +52,6 @@ export const LayersList = () => { const LayerCard = ({ index, layer }) => { const { layerTypes, - removeLayer, swapLayers, toggleLayerVisibility, } = useLayers(); @@ -61,10 +59,6 @@ const LayerCard = ({ index, layer }) => { const isVisible = layer.state.visible; const LayerIcon = layerTypes[layer.properties.product_type].icon; - const handleClickRemove = id => () => { - removeLayer(id); - }; - return ( { }} > - - + {layerTypes[layer.properties.product_type].name} + toggleLayerVisibility(layer.id) } + className="actions" + /> - - - - + { new Date(layer.properties.run_date).toLocaleString() } - - Cycle { layer.properties.cycle } - + + + Cycle { layer.properties.cycle } - {/* layer card actions start */} - - toggleLayerVisibility(layer.id) } - /> - - - - - - - - - - - - - - - { > - {/* layer card actions end */} - { + { + const { removeLayer } = useLayers(); + + return ( + + + + + + + + + + + + removeLayer(layerId) } + > + + + + ); +}; + +LayerActions.propTypes = { + layerId: PropTypes.string.isRequired, +}; From ba334e479e3759f8745cacadc21d356fa19f9e39 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sun, 5 May 2024 17:22:31 -0400 Subject: [PATCH 12/17] create action button components --- src/components/buttons/action-button-menu.js | 25 ++++++++++++++++++++ src/components/buttons/action-button.js | 23 ++++++++++++++++++ src/components/buttons/index.js | 2 ++ 3 files changed, 50 insertions(+) create mode 100644 src/components/buttons/action-button-menu.js create mode 100644 src/components/buttons/action-button.js create mode 100644 src/components/buttons/index.js diff --git a/src/components/buttons/action-button-menu.js b/src/components/buttons/action-button-menu.js new file mode 100644 index 00000000..a5b075be --- /dev/null +++ b/src/components/buttons/action-button-menu.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { Sheet, Stack } from '@mui/joy'; +import PropTypes from 'prop-types'; + +export const ActionButtonMenu = ({ children }) => { + return ( + + { children } + + ); +}; + +ActionButtonMenu.propTypes = { + children: PropTypes.node.isRequired, +}; diff --git a/src/components/buttons/action-button.js b/src/components/buttons/action-button.js new file mode 100644 index 00000000..cdb4e6db --- /dev/null +++ b/src/components/buttons/action-button.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { IconButton } from '@mui/joy'; + +export const ActionButton = ({ + children, + ...props +}) => { + return ( + + { children } + + ); +}; + +ActionButton.propTypes = { + children: PropTypes.node, +}; diff --git a/src/components/buttons/index.js b/src/components/buttons/index.js new file mode 100644 index 00000000..e7c9bed6 --- /dev/null +++ b/src/components/buttons/index.js @@ -0,0 +1,2 @@ +export * from './action-button'; +export * from './action-button-menu'; From d5677c29f918351da9c7f7ddaa3776d51dab74d1 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sun, 5 May 2024 17:22:45 -0400 Subject: [PATCH 13/17] refactor, use action buttons --- .../trays/layers/layer-card-actions.js | 46 +++++ src/components/trays/layers/layer-card.js | 150 ++++++++++++++ src/components/trays/layers/list.js | 184 +----------------- 3 files changed, 197 insertions(+), 183 deletions(-) create mode 100644 src/components/trays/layers/layer-card-actions.js create mode 100644 src/components/trays/layers/layer-card.js diff --git a/src/components/trays/layers/layer-card-actions.js b/src/components/trays/layers/layer-card-actions.js new file mode 100644 index 00000000..f2aab56f --- /dev/null +++ b/src/components/trays/layers/layer-card-actions.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Divider } from '@mui/joy'; +import { + DeleteForever as RemoveIcon, + Opacity as OpacityIcon, + Palette as ColorRampIcon, + DataObject as RawDataIcon, +} from '@mui/icons-material'; +import { useLayers } from '@context'; +import { ActionButton } from '@components/buttons'; +import { ActionButtonMenu } from '@components/buttons'; + +export const LayerActions = ({ layerId = 0 }) => { + const { removeLayer } = useLayers(); + + return ( + + + + + + + + + + + + + + + + removeLayer(layerId) } + > + + + + ); +}; + +LayerActions.propTypes = { + layerId: PropTypes.string.isRequired, +}; diff --git a/src/components/trays/layers/layer-card.js b/src/components/trays/layers/layer-card.js new file mode 100644 index 00000000..2ecdb34b --- /dev/null +++ b/src/components/trays/layers/layer-card.js @@ -0,0 +1,150 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Accordion, + AccordionDetails, + Avatar, + Box, + ButtonGroup, + Stack, + Switch, + Typography, +} from '@mui/joy'; +import { + KeyboardArrowDown as ExpandIcon, + ArrowDropUp as MoveUpArrow, + ArrowDropDown as MoveDownArrow, + Schedule as ClockIcon, +} from '@mui/icons-material'; +import { useLayers } from '@context'; +import { useToggleState } from '@hooks'; +import { LayerActions } from './layer-card-actions'; +import { ActionButton } from '@components/buttons'; + +export const LayerCard = ({ index, layer }) => { + const { + layerTypes, + swapLayers, + toggleLayerVisibility, + } = useLayers(); + const expanded = useToggleState(false); + const isVisible = layer.state.visible; + const LayerIcon = layerTypes[layer.properties.product_type].icon; + + return ( + + {/* + the usual AccordionSummary component results in a button, + but we want some buttons _inside_ the accordion summary, + so we'll build a custom component here. + */} + + + + + + + + {layerTypes[layer.properties.product_type].name} + + toggleLayerVisibility(layer.id) } + className="action-button" + /> + + + + + { new Date(layer.properties.run_date).toLocaleString() } + + + Cycle { layer.properties.cycle } + + + + + + swapLayers(index, index - 1) } + > + swapLayers(index, index + 1) } + > + + + + + + + + + + { JSON.stringify(layer.properties, null, 2) } + + + + + ); +}; + +LayerCard.propTypes = { + index: PropTypes.number.isRequired, + layer: PropTypes.object.isRequired, +}; + diff --git a/src/components/trays/layers/list.js b/src/components/trays/layers/list.js index d7782f9f..94b5c01c 100644 --- a/src/components/trays/layers/list.js +++ b/src/components/trays/layers/list.js @@ -1,29 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { - Accordion, AccordionGroup, - AccordionDetails, - Avatar, - Box, - ButtonGroup, Divider, - IconButton, - Stack, - Switch, - Typography, } from '@mui/joy'; -import { - DeleteForever as RemoveIcon, - KeyboardArrowDown as ExpandIcon, - ArrowDropUp as MoveUpArrow, - ArrowDropDown as MoveDownArrow, - Schedule as ClockIcon, - Opacity as OpacityIcon, - Palette as ColorRampIcon, -} from '@mui/icons-material'; import { useLayers } from '@context'; -import { useToggleState } from '@hooks'; +import { LayerCard } from './layer-card'; export const LayersList = () => { const { defaultModelLayers } = useLayers(); @@ -49,166 +30,3 @@ export const LayersList = () => { ); }; -const LayerCard = ({ index, layer }) => { - const { - layerTypes, - swapLayers, - toggleLayerVisibility, - } = useLayers(); - const expanded = useToggleState(false); - const isVisible = layer.state.visible; - const LayerIcon = layerTypes[layer.properties.product_type].icon; - - return ( - - {/* - the usual AccordionSummary component results in a button, - but we want some buttons _inside_ the accordion summary, - so we'll build a custom component here. - */} - - - - - - - - {layerTypes[layer.properties.product_type].name} - - toggleLayerVisibility(layer.id) } - className="actions" - /> - - - - - { new Date(layer.properties.run_date).toLocaleString() } - - - Cycle { layer.properties.cycle } - - - - - - swapLayers(index, index - 1) } - sx={{ flex: 1 }} - > - swapLayers(index, index + 1) } - sx={{ flex: 1 }} - > - - - - - - - - - - { JSON.stringify(layer.properties, null, 2) } - - - - - ); -}; - -LayerCard.propTypes = { - index: PropTypes.number.isRequired, - layer: PropTypes.object.isRequired, -}; - -const LayerActions = ({ layerId = 0 }) => { - const { removeLayer } = useLayers(); - - return ( - - - - - - - - - - - - removeLayer(layerId) } - > - - - - ); -}; - -LayerActions.propTypes = { - layerId: PropTypes.string.isRequired, -}; From 55a7699e62132ee659ce162f35a54aa01207dc48 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Sun, 5 May 2024 17:31:32 -0400 Subject: [PATCH 14/17] remove unused component --- src/components/sidebar/toggler.js | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/components/sidebar/toggler.js diff --git a/src/components/sidebar/toggler.js b/src/components/sidebar/toggler.js deleted file mode 100644 index f76bee07..00000000 --- a/src/components/sidebar/toggler.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { IconButton } from '@mui/joy'; -import { - Menu as HamburgerIcon, - Close as CloseMenuIcon, -} from '@mui/icons-material'; -import { useLayout } from '@context'; - -export const DrawerToggler = () => { - const { drawer } = useLayout(); - - return ( - - { drawer.isOpen ? : } - - ); -}; From 9220b41ed859246b1991e326193a55104854036c Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Mon, 6 May 2024 00:41:31 -0400 Subject: [PATCH 15/17] set up theme and dark mode toggle cleanup --- src/components/buttons/action-button-menu.js | 2 +- src/components/map/map.js | 14 +- src/components/sidebar/sidebar.js | 8 +- src/components/trays/layers/layer-card.js | 13 +- src/components/trays/settings/dark-mode.js | 49 ++++++ src/components/trays/settings/index.js | 4 +- src/context/settings.js | 21 ++- src/index.js | 17 ++- src/theme.js | 153 +++++++++++++++++++ 9 files changed, 260 insertions(+), 21 deletions(-) create mode 100644 src/components/trays/settings/dark-mode.js create mode 100644 src/theme.js diff --git a/src/components/buttons/action-button-menu.js b/src/components/buttons/action-button-menu.js index a5b075be..a564e330 100644 --- a/src/components/buttons/action-button-menu.js +++ b/src/components/buttons/action-button-menu.js @@ -12,7 +12,7 @@ export const ActionButtonMenu = ({ children }) => { className="action-button-menu" sx={{ p: 1, - backgroundColor: 'neutral.softActiveBg', + backgroundColor: 'background.surface', }} > { children } diff --git a/src/components/map/map.js b/src/components/map/map.js index d54bf614..1fe8a53c 100644 --- a/src/components/map/map.js +++ b/src/components/map/map.js @@ -1,12 +1,16 @@ import React from 'react'; import { MapContainer, TileLayer } from 'react-leaflet'; import { DefaultLayers } from './default-layers'; -import { useLayers } from '@context'; +import { + useLayers, + useSettings, +} from '@context'; import 'leaflet/dist/leaflet.css'; const DEFAULT_CENTER = [30.0, -73.0]; export const Map = () => { + const { darkMode } = useSettings(); const { setMap } = useLayers(); @@ -19,10 +23,10 @@ export const Map = () => { scrollWheelZoom={true} whenCreated={setMap} style={{ height: '100vh', width:'100wh' }}> - + { darkMode.enabled + ? + : + } ); diff --git a/src/components/sidebar/sidebar.js b/src/components/sidebar/sidebar.js index ddc8a92b..ebd34f1f 100644 --- a/src/components/sidebar/sidebar.js +++ b/src/components/sidebar/sidebar.js @@ -3,12 +3,14 @@ import { Fragment, useCallback, useState } from 'react'; import { List, Sheet, + useTheme, } from '@mui/joy'; import { Tray } from './tray'; import { MenuItem } from './menu-item'; import SidebarTrays from '../trays'; export const Sidebar = () => { + const theme = useTheme(); const [activeIndex, setActiveIndex] = useState(0); const handleClickMenuItem = useCallback(newIndex => { @@ -34,13 +36,15 @@ export const Sidebar = () => { maxWidth: '68px', overflow: 'hidden', p: 0, - backgroundColor: activeIndex === -1 ? '#f0f4f899' : '#f0f4f8', + backgroundColor: activeIndex === -1 + ? `rgba(${ theme.palette.mainChannel } / 0.2)` + : `rgba(${ theme.palette.mainChannel } / 1.0)`, // a drop shadow looks nice. we'll remove it if a tray is open, // as they should appear on the same plane. filter: activeIndex === -1 ? 'drop-shadow(0 0 8px rgba(0, 0, 0, 0.2))' : 'drop-shadow(1px 0 0 rgba(0, 0, 0, 0.2))', // similarly, here. '&:hover': { - backgroundColor: '#f0f4f8', + backgroundColor: `rgba(${ theme.palette.mainChannel } / 1.0)`, transition: 'max-width 250ms, filter 250ms, background-color 150ms', }, // we'll add a delay to this exit animation to give ample time diff --git a/src/components/trays/layers/layer-card.js b/src/components/trays/layers/layer-card.js index 2ecdb34b..0247da06 100644 --- a/src/components/trays/layers/layer-card.js +++ b/src/components/trays/layers/layer-card.js @@ -48,7 +48,12 @@ export const LayerCard = ({ index, layer }) => { sx={{ p: 1, borderLeft: '6px solid', - borderLeftColor: isVisible ? 'primary.400' : 'primary.100', + // borderLeftColor: isVisible + // ? `rgba(${ theme.palette.primary.mainChannel }) / 1.0` + // : `rgba(${ theme.palette.primary.mainChannel }) / 0.2`, + borderLeftColor: isVisible + ? `primary.plainColor` + : `primary.plainDisabledColor`, '.action-button': { filter: 'opacity(0.1)', transition: 'filter 250ms' }, '&:hover .action-button': { filter: 'opacity(0.5)' }, '& .action-button:hover': { filter: 'opacity(1.0)' }, @@ -119,7 +124,8 @@ export const LayerCard = ({ index, layer }) => { /> - { { JSON.stringify(layer.properties, null, 2) } diff --git a/src/components/trays/settings/dark-mode.js b/src/components/trays/settings/dark-mode.js new file mode 100644 index 00000000..f611cb42 --- /dev/null +++ b/src/components/trays/settings/dark-mode.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { IconButton, Stack, Typography } from '@mui/joy'; +import { + DarkMode as OnIcon, + LightMode as OffIcon, +} from '@mui/icons-material'; +import { useSettings } from '@context'; + +export const DarkModeToggle = () => { + const { darkMode } = useSettings(); + + return ( + + +
+ + Dark Mode + + + { darkMode.enabled ? 'Enabled' : 'Disabled' } + +
+
+ ); +}; + +export const Toggler = () => { + const { darkMode } = useSettings(); + + return ( + + { + darkMode.enabled + ? + : + } + + ); +}; diff --git a/src/components/trays/settings/index.js b/src/components/trays/settings/index.js index 464bf421..1cb45672 100644 --- a/src/components/trays/settings/index.js +++ b/src/components/trays/settings/index.js @@ -2,7 +2,7 @@ import React from 'react'; import { Stack } from '@mui/joy'; import { Tune as SettingsIcon } from '@mui/icons-material'; -import { SampleToggle } from './sample'; +import { DarkModeToggle } from './dark-mode'; export const icon = ; @@ -10,6 +10,6 @@ export const title = 'Settings'; export const trayContents = () => ( - + ); diff --git a/src/context/settings.js b/src/context/settings.js index a3bdd0a2..d4018f85 100644 --- a/src/context/settings.js +++ b/src/context/settings.js @@ -1,5 +1,11 @@ -import React, { createContext, useContext } from "react"; +import React, { + createContext, + useCallback, + useContext, + useMemo, +} from "react"; import PropTypes from "prop-types"; +import { useColorScheme } from '@mui/joy/styles'; import { // useToggleLocalStorage, useToggleState, @@ -9,13 +15,24 @@ export const SettingsContext = createContext({}); export const useSettings = () => useContext(SettingsContext); export const SettingsProvider = ({ children }) => { + const { mode, setMode } = useColorScheme(); const booleanValue = useToggleState(); // to persist the value in the device's local // storage, use `useToggleLocalStorage` instead: // const booleanValue = useToggleLocalStorage('boolean-value') + const darkMode = useMemo(() => mode === 'dark', [mode]); + const toggleDarkMode = useCallback(() => { + setMode(darkMode ? 'light' : 'dark'); + }, [mode]); return ( - + { children } ); diff --git a/src/index.js b/src/index.js index d5ea0a3f..42df9d84 100644 --- a/src/index.js +++ b/src/index.js @@ -1,18 +1,23 @@ import React from 'react'; -import { App } from './app'; +import { CssVarsProvider } from '@mui/joy/styles'; import { createRoot } from 'react-dom/client'; +import { App } from './app'; import { LayersProvider, SettingsProvider } from '@context'; import './index.css'; +import '@fontsource/inter'; +import theme from './theme'; const container = document.getElementById('root'); const root = createRoot(container); const ProvisionedApp = () => ( - - - - - + + + + + + + ); root.render(); diff --git a/src/theme.js b/src/theme.js new file mode 100644 index 00000000..ee635cf8 --- /dev/null +++ b/src/theme.js @@ -0,0 +1,153 @@ +import { extendTheme } from '@mui/joy/styles'; + +const theme = extendTheme({ + breakpoints: { + keys: ['xs', 'sm', 'md', 'lg', 'xl'], + values: { xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 }, + unit: 'px', + }, + defaultMode: 'system', + direction: 'ltr', + palette: { + mode: 'light', + primary: { + 50: '#f0f7ff', + 100: '#c2e0ff', + 200: '#99ccf3', + 300: '#66b2ff', + 400: '#3399ff', + 500: '#007fff', + 600: '#0072e5', + 700: '#0059b2', + 800: '#004c99', + 900: '#003a75', + main: '#007fff', + light: '#66b2ff', + dark: '#0059b2', + contrastText: '#fff' + }, + secondary: { + 50: '#f3f6f9', + 100: '#e5eaf2', + 200: '#dae2ed', + 300: '#c7d0dd', + 400: '#b0b8c4', + 500: '#9da8b7', + 600: '#6b7a90', + 700: '#434d5b', + 800: '#303740', + 900: '#1c2025', + main: '#dae0e7', + contrastText: '#2f3a46', + light: 'rgb(225, 230, 235)', + dark: 'rgb(152, 156, 161)' + }, + divider: '#e5eaf2', + primaryDark: { + 50: '#eaedf1', + 100: '#dae0e7', + 200: '#acbac8', + 300: '#7b91a7', + 400: '#4b5e71', + 500: '#3b4a59', + 600: '#2f3a46', + 700: '#1f262e', + 800: '#141a1f', + 900: '#101418', + main: '#7b91a7' + }, + common: { + black: '#0b0d0e', + white: '#fff' + }, + text: { + primary: '#1c2025', + secondary: '#434d5b', + tertiary: '#6b7a90', + disabled: 'rgba(0, 0, 0, 0.38)' + }, + grey: { + 50: '#f3f6f9', + 100: '#e5eaf2', + 200: '#dae2ed', + 300: '#c7d0dd', + 400: '#b0b8c4', + 500: '#9da8b7', + 600: '#6b7a90', + 700: '#434d5b', + 800: '#303740', + 900: '#1c2025', + main: '#e5eaf2', + contrastText: '#6b7a90', + A100: '#f5f5f5', + A200: '#eeeeee', + A400: '#bdbdbd', + A700: '#616161' + }, + error: { + 50: '#fff0f1', + 100: '#ffdbde', + 200: '#ffbdc2', + 300: '#ff99a2', + 400: '#ff7a86', + 500: '#ff505f', + 600: '#eb0014', + 700: '#c70011', + 800: '#94000d', + 900: '#570007', + main: '#eb0014', + light: '#ff99a2', + dark: '#c70011', + contrastText: '#fff' + }, + success: { + 50: '#e9fbf0', + 100: '#c6f6d9', + 200: '#9aefbc', + 300: '#6ae79c', + 400: '#3ee07f', + 500: '#21cc66', + 600: '#1db45a', + 700: '#1aa251', + 800: '#178d46', + 900: '#0f5c2e', + main: '#1aa251', + light: '#6ae79c', + dark: '#1aa251', + contrastText: '#fff' + }, + warning: { + 50: '#fff9eb', + 100: '#fff3c1', + 200: '#ffeca1', + 300: '#ffdc48', + 400: '#f4c000', + 500: '#dea500', + 600: '#d18e00', + 700: '#ab6800', + 800: '#8c5800', + 900: '#5a3600', + main: '#dea500', + light: '#ffdc48', + dark: '#ab6800', + contrastText: 'rgba(0, 0, 0, 0.87)' + }, + info: { + main: '#0288d1', + light: '#03a9f4', + dark: '#01579b', + contrastText: '#fff' + }, + contrastThreshold: 3, + tonalOffset: 0.2, + background: { + paper: '#fff', + default: '#fff' + }, + }, + shape: { + borderRadius: 12 + }, +}); + +export default theme; From 179f8183ca3f3d386e206b6a57d78959e9735007 Mon Sep 17 00:00:00 2001 From: Matt Watson Date: Mon, 6 May 2024 10:36:49 -0400 Subject: [PATCH 16/17] use mapbox token env var --- src/components/map/map.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/map/map.js b/src/components/map/map.js index 1fe8a53c..1bec5aca 100644 --- a/src/components/map/map.js +++ b/src/components/map/map.js @@ -24,8 +24,8 @@ export const Map = () => { whenCreated={setMap} style={{ height: '100vh', width:'100wh' }}> { darkMode.enabled - ? - : + ? + : } From 1b195b25649147db8e8396926789778513e8cda3 Mon Sep 17 00:00:00 2001 From: Phil Owen <19691521+PhillipsOwen@users.noreply.github.com> Date: Tue, 7 May 2024 10:13:27 -0400 Subject: [PATCH 17/17] adjusting sizing --- src/components/dialog/base-floating-dialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/dialog/base-floating-dialog.js b/src/components/dialog/base-floating-dialog.js index 6652ce2c..6f89682c 100644 --- a/src/components/dialog/base-floating-dialog.js +++ b/src/components/dialog/base-floating-dialog.js @@ -63,8 +63,8 @@ export default function BaseFloatingDialog({ title, dialogObject, dataKey, dataL TransitionComponent={Transition} disableEnforceFocus style={{ pointerEvents: 'none' }} - PaperProps={{ sx: { width: 1400, height: 510, pointerEvents: 'auto'} }} - sx={{ width: 1500, height: 510, '.MuiBackdrop-root': { backgroundColor: 'transparent' }}} + PaperProps={{ sx: { width: 625, height: 510, pointerEvents: 'auto'} }} + sx={{ width: 625, height: 510, '.MuiBackdrop-root': { backgroundColor: 'transparent' }}} > { title }