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 && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+}