diff --git a/API/Backend/Geodatasets/routes/geodatasets.js b/API/Backend/Geodatasets/routes/geodatasets.js index 94364ad5..91a71917 100644 --- a/API/Backend/Geodatasets/routes/geodatasets.js +++ b/API/Backend/Geodatasets/routes/geodatasets.js @@ -8,6 +8,7 @@ const router = express.Router(); const { sequelize } = require("../../../connection"); const logger = require("../../../logger"); +const Utils = require("../../../utils"); const geodatasets = require("../models/geodatasets"); const Geodatasets = geodatasets.Geodatasets; const makeNewGeodatasetTable = geodatasets.makeNewGeodatasetTable; @@ -206,10 +207,51 @@ router.post("/entries", function (req, res, next) { for (let i = 0; i < sets.length; i++) { entries.push({ name: sets[i].name, updated: sets[i].updatedAt }); } - res.send({ - status: "success", - body: { entries: entries }, - }); + // For each entry, list all occurrences in latest configuration objects + sequelize + .query( + "SELECT DISTINCT ON (mission) mission, version, config FROM configs ORDER BY mission ASC" + ) + .then(([results]) => { + // Populate occurrences + results.forEach((m) => { + Utils.traverseLayers([m.config.layers], (layer, path) => { + entries.forEach((entry) => { + entry.occurrences = entry.occurrences || {}; + entry.occurrences[m.mission] = + entry.occurrences[m.mission] || []; + + if (layer.url === `geodatasets:${entry.name}`) { + entry.occurrences[m.mission].push({ + name: layer.name, + uuid: layer.uuid, + path: path, + }); + } + }); + }); + }); + + res.send({ + status: "success", + body: { entries: entries }, + }); + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to find missions.", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to find missions.", + }); + return null; + }); } else { res.send({ status: "failure", @@ -330,7 +372,7 @@ router.post("/recreate", function (req, res, next) { } let drop_qry = "TRUNCATE TABLE " + result.table + " RESTART IDENTITY"; - if (req.body.hasOwnProperty("action") && req.body.action=="append") { + if (req.body.hasOwnProperty("action") && req.body.action == "append") { drop_qry = ""; } diff --git a/configure/src/components/Main/Main.js b/configure/src/components/Main/Main.js index 194e39fb..1e36dc71 100644 --- a/configure/src/components/Main/Main.js +++ b/configure/src/components/Main/Main.js @@ -26,6 +26,7 @@ import Time from "../Tabs/Time/Time"; import UserInterface from "../Tabs/UserInterface/UserInterface"; import APITokens from "../../pages/APITokens/APITokens"; +import GeoDatasets from "../../pages/GeoDatasets/GeoDatasets"; const useStyles = makeStyles((theme) => ({ Main: { @@ -126,6 +127,9 @@ export default function Main() { let Page = null; switch (page) { + case "geodatasets": + Page = ; + break; case "api_tokens": Page = ; break; diff --git a/configure/src/components/Panel/Panel.js b/configure/src/components/Panel/Panel.js index 19e142eb..b6ebf50f 100644 --- a/configure/src/components/Panel/Panel.js +++ b/configure/src/components/Panel/Panel.js @@ -162,7 +162,7 @@ export default function Panel() { disableElevation startIcon={} onClick={() => { - dispatch(setModal({ name: "newMission" })); + dispatch(setPage({ page: "geodatasets" })); }} > GeoDatasets @@ -173,7 +173,7 @@ export default function Panel() { disableElevation startIcon={} onClick={() => { - dispatch(setModal({ name: "newMission" })); + dispatch(setPage({ page: "newMission" })); }} > Datasets @@ -195,7 +195,7 @@ export default function Panel() { disableElevation startIcon={} onClick={() => { - dispatch(setModal({ name: "newMission" })); + dispatch(setPage({ page: "newMission" })); }} > WebHooks diff --git a/configure/src/core/ConfigureStore.js b/configure/src/core/ConfigureStore.js index 33aa0b44..a20eefb0 100644 --- a/configure/src/core/ConfigureStore.js +++ b/configure/src/core/ConfigureStore.js @@ -7,6 +7,7 @@ export const ConfigureStore = createSlice({ mission: null, configuration: "{}", toolConfiguration: {}, + geodatasets: [], page: null, modal: { newMission: false, @@ -31,6 +32,9 @@ export const ConfigureStore = createSlice({ setToolConfiguration: (state, action) => { state.toolConfiguration = action.payload; }, + setGeodatasets: (state, action) => { + state.geodatasets = action.payload; + }, setPage: (state, action) => { state.page = action.payload.page; }, @@ -66,6 +70,7 @@ export const { setMission, setConfiguration, setToolConfiguration, + setGeodatasets, setPage, setModal, setSnackBarText, diff --git a/configure/src/pages/GeoDatasets/GeoDatasets.js b/configure/src/pages/GeoDatasets/GeoDatasets.js new file mode 100644 index 00000000..f0df1c24 --- /dev/null +++ b/configure/src/pages/GeoDatasets/GeoDatasets.js @@ -0,0 +1,397 @@ +import React, { useEffect, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { makeStyles } from "@mui/styles"; + +import clsx from "clsx"; + +import { calls } from "../../core/calls"; +import { copyToClipboard } from "../../core/utils"; +import { setSnackBarText, setGeodatasets } from "../../core/ConfigureStore"; + +import PropTypes from "prop-types"; +import { alpha } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TablePagination from "@mui/material/TablePagination"; +import TableRow from "@mui/material/TableRow"; +import TableSortLabel from "@mui/material/TableSortLabel"; +import Toolbar from "@mui/material/Toolbar"; +import Typography from "@mui/material/Typography"; +import Paper from "@mui/material/Paper"; +import Checkbox from "@mui/material/Checkbox"; +import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; +import DeleteIcon from "@mui/icons-material/Delete"; +import FilterListIcon from "@mui/icons-material/FilterList"; +import { visuallyHidden } from "@mui/utils"; + +function descendingComparator(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === "desc" + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +// Since 2020 all major browsers ensure sort stability with Array.prototype.sort(). +// stableSort() brings sort stability to non-modern browsers (notably IE11). If you +// only support modern browsers you can replace stableSort(exampleArray, exampleComparator) +// with exampleArray.slice().sort(exampleComparator) +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) { + return order; + } + return a[1] - b[1]; + }); + return stabilizedThis.map((el) => el[0]); +} + +const headCells = [ + { + id: "name", + numeric: false, + disablePadding: true, + label: "GeoDataset Name", + }, + { + id: "num_features", + numeric: true, + disablePadding: false, + label: "Number of Features", + }, + { + id: "updated", + numeric: false, + disablePadding: false, + label: "Last Updated", + }, + { + id: "actions", + numeric: false, + disablePadding: false, + label: "Actions", + }, +]; + +function EnhancedTableHead(props) { + const { + onSelectAllClick, + order, + orderBy, + numSelected, + rowCount, + onRequestSort, + } = props; + const createSortHandler = (property) => (event) => { + onRequestSort(event, property); + }; + + return ( + + + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + inputProps={{ + "aria-label": "select all desserts", + }} + /> + + {headCells.map((headCell) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === "desc" ? "sorted descending" : "sorted ascending"} + + ) : null} + + + ))} + + + ); +} + +EnhancedTableHead.propTypes = { + numSelected: PropTypes.number.isRequired, + onRequestSort: PropTypes.func.isRequired, + onSelectAllClick: PropTypes.func.isRequired, + order: PropTypes.oneOf(["asc", "desc"]).isRequired, + orderBy: PropTypes.string.isRequired, + rowCount: PropTypes.number.isRequired, +}; + +function EnhancedTableToolbar(props) { + const { numSelected } = props; + + return ( + 0 && { + bgcolor: (theme) => + alpha( + theme.palette.primary.main, + theme.palette.action.activatedOpacity + ), + }), + }} + > + {numSelected > 0 ? ( + + {numSelected} selected + + ) : ( + + GEODATASETS + + )} + + {numSelected > 0 ? ( + + + + + + ) : ( + + + + + + )} + + ); +} + +EnhancedTableToolbar.propTypes = { + numSelected: PropTypes.number.isRequired, +}; + +export default function GeoDatasets() { + const [order, setOrder] = React.useState("asc"); + const [orderBy, setOrderBy] = React.useState("calories"); + const [selected, setSelected] = React.useState([]); + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(5); + + const dispatch = useDispatch(); + const geodatasets = useSelector((state) => state.core.geodatasets); + useEffect(() => { + calls.api( + "geodatasets_entries", + {}, + (res) => { + if (res.status === "success") + dispatch( + setGeodatasets( + res.body.entries.map((en, idx) => { + en.id = idx; + return en; + }) + ) + ); + else + dispatch( + setSnackBarText({ + text: res?.message || "Failed to get geodatasets.", + severity: "error", + }) + ); + }, + (res) => { + dispatch( + setSnackBarText({ + text: res?.message || "Failed to get geodatasets.", + severity: "error", + }) + ); + } + ); + }, [dispatch]); + + console.log(geodatasets); + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelected = geodatasets.map((n) => n.id); + setSelected(newSelected); + return; + } + setSelected([]); + }; + + const handleClick = (event, id) => { + const selectedIndex = selected.indexOf(id); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, id); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + setSelected(newSelected); + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const isSelected = (id) => selected.indexOf(id) !== -1; + + // Avoid a layout jump when reaching the last page with empty rows. + const emptyRows = + page > 0 ? Math.max(0, (1 + page) * rowsPerPage - geodatasets.length) : 0; + + const visibleRows = React.useMemo( + () => + stableSort(geodatasets, getComparator(order, orderBy)).slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ), + [order, orderBy, page, rowsPerPage, geodatasets] + ); + + return ( + + + + + + + + {visibleRows.map((row, index) => { + const isItemSelected = isSelected(row.id); + const labelId = `enhanced-table-checkbox-${index}`; + + return ( + handleClick(event, row.id)} + role="checkbox" + aria-checked={isItemSelected} + tabIndex={-1} + key={row.id} + selected={isItemSelected} + sx={{ cursor: "pointer" }} + > + + + + + {row.name} + + {row.calories} + {row.fat} + {row.carbs} + {row.protein} + + ); + })} + {emptyRows > 0 && ( + + + + )} + +
+
+ +
+
+ ); +}