diff --git a/API/Backend/Config/uuids.js b/API/Backend/Config/uuids.js
index 0d761abf..87af4590 100644
--- a/API/Backend/Config/uuids.js
+++ b/API/Backend/Config/uuids.js
@@ -9,13 +9,17 @@ const populateUUIDs = (config) => {
// Track of all of the previously defined UUIDs (i.e. ignore the UUIDs of the newly added layers)
Utils.traverseLayers(config.layers, (layer) => {
- if (layer.uuid != null && !layer.proposed_uuid) {
+ if (
+ layer.uuid != null &&
+ typeof layer.uuid !== "number" &&
+ !layer.proposed_uuid
+ ) {
definedUUIDs.push(layer.uuid);
}
});
Utils.traverseLayers(config.layers, (layer) => {
- if (layer.uuid == null) {
+ if (layer.uuid == null || typeof layer.uuid === "number") {
layer.uuid = uuidv4();
newlyAddedUUIDs.push({
name: layer.name,
diff --git a/configure/src/components/Main/Main.js b/configure/src/components/Main/Main.js
index 06246844..8db523f0 100644
--- a/configure/src/components/Main/Main.js
+++ b/configure/src/components/Main/Main.js
@@ -37,6 +37,7 @@ import UserInterface from "../Tabs/UserInterface/UserInterface";
import APITokens from "../../pages/APITokens/APITokens";
import GeoDatasets from "../../pages/GeoDatasets/GeoDatasets";
import Datasets from "../../pages/Datasets/Datasets";
+import WebHooks from "../../pages/WebHooks/WebHooks";
const useStyles = makeStyles((theme) => ({
Main: {
@@ -163,6 +164,9 @@ export default function Main() {
case "api_tokens":
Page = ;
break;
+ case "webhooks":
+ Page = ;
+ break;
default:
}
diff --git a/configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js b/configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js
index 1a3cbb8a..bb42b56c 100644
--- a/configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js
+++ b/configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js
@@ -76,7 +76,6 @@ const useStyles = makeStyles((theme) => ({
subtitle: {
fontSize: "14px !important",
width: "100%",
- textAlign: "right",
marginBottom: "8px !important",
color: theme.palette.swatches.grey[300],
letterSpacing: "0.2px",
@@ -129,7 +128,10 @@ const NewMissionModal = (props) => {
calls.api(
"add",
- null,
+ {
+ mission: missionName,
+ makedir: createDir,
+ },
(res) => {
calls.api(
"missions",
diff --git a/configure/src/components/Panel/Panel.js b/configure/src/components/Panel/Panel.js
index 325c8b44..56a8a867 100644
--- a/configure/src/components/Panel/Panel.js
+++ b/configure/src/components/Panel/Panel.js
@@ -6,7 +6,12 @@ import mmgisLogo from "../../images/mmgis.png";
import clsx from "clsx";
-import { setMission, setModal, setPage } from "../../core/ConfigureStore";
+import {
+ setConfiguration,
+ setMission,
+ setModal,
+ setPage,
+} from "../../core/ConfigureStore";
import NewMissionModal from "./Modals/NewMissionModal/NewMissionModal";
@@ -177,6 +182,7 @@ export default function Panel() {
disableElevation
startIcon={}
onClick={() => {
+ dispatch(setMission(null));
dispatch(setPage({ page: "geodatasets" }));
}}
>
@@ -188,6 +194,7 @@ export default function Panel() {
disableElevation
startIcon={}
onClick={() => {
+ dispatch(setMission(null));
dispatch(setPage({ page: "datasets" }));
}}
>
@@ -199,6 +206,7 @@ export default function Panel() {
disableElevation
startIcon={}
onClick={() => {
+ dispatch(setMission(null));
dispatch(setPage({ page: "api_tokens" }));
}}
>
@@ -210,6 +218,7 @@ export default function Panel() {
disableElevation
startIcon={}
onClick={() => {
+ dispatch(setMission(null));
dispatch(setPage({ page: "webhooks" }));
}}
>
diff --git a/configure/src/components/SaveBar/Modals/PreviewModal/PreviewModal.js b/configure/src/components/SaveBar/Modals/PreviewModal/PreviewModal.js
index 323cd32f..22c6e3a6 100644
--- a/configure/src/components/SaveBar/Modals/PreviewModal/PreviewModal.js
+++ b/configure/src/components/SaveBar/Modals/PreviewModal/PreviewModal.js
@@ -154,7 +154,11 @@ const PreviewModal = (props) => {
-
Previewing Changes
+
+ {modal?.customConfig != null
+ ? `Previewing Configuration v${modal.version}`
+ : "Previewing Changes"}
+
{
-
+
);
diff --git a/configure/src/components/Tabs/Home/Home.js b/configure/src/components/Tabs/Home/Home.js
index 52613fb8..32398123 100644
--- a/configure/src/components/Tabs/Home/Home.js
+++ b/configure/src/components/Tabs/Home/Home.js
@@ -4,20 +4,73 @@ import { setVersions } from "./HomeSlice";
import { makeStyles } from "@mui/styles";
import { calls } from "../../../core/calls";
+import { downloadObject } from "../../../core/utils";
import Maker from "../../../core/Maker";
-import { setSnackBarText } from "../../../core/ConfigureStore";
+import { setSnackBarText, setModal } from "../../../core/ConfigureStore";
+
+import Versions from "./Versions";
+
+import IconButton from "@mui/material/IconButton";
+import Tooltip from "@mui/material/Tooltip";
+
+import BrowserUpdatedIcon from "@mui/icons-material/BrowserUpdated";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+import UploadIcon from "@mui/icons-material/Upload";
+import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
import config from "../../../metaconfigs/tab-home-config.json";
+import UploadConfigModal from "./Modals/UploadConfigModal/UploadConfigModal";
+import CloneConfigModal from "./Modals/CloneConfigModal/CloneConfigModal";
+import DeleteConfigModal from "./Modals/DeleteConfigModal/DeleteConfigModal";
const useStyles = makeStyles((theme) => ({
Home: {
width: "100%",
display: "flex",
+ flexFlow: "column",
background: theme.palette.swatches.grey[1000],
padding: "0px 32px 64px 32px",
boxSizing: "border-box",
backgroundImage: "url(configure/build/gridlines.png)",
},
+ top: {
+ display: "flex",
+ justifyContent: "space-between",
+ margin: "20px 0px 8px 0px",
+ },
+ title: {
+ letterSpacing: "2px",
+ color: theme.palette.swatches.p[0],
+ textShadow: `0px 2px 1px ${theme.palette.swatches.grey[300]}`,
+ fontSize: "48px",
+ margin: 0,
+ },
+ right: {
+ display: "flex",
+ },
+ exportIcon: {
+ color: `${theme.palette.swatches.p[11]} !important`,
+ width: "40px",
+ height: "40px",
+ margin: "9px !important",
+ },
+ uploadIcon: {
+ width: "40px",
+ height: "40px",
+ margin: "9px !important",
+ },
+ cloneIcon: {
+ color: `${theme.palette.accent.main} !important`,
+ width: "40px",
+ height: "40px",
+ margin: "9px !important",
+ },
+ deleteIcon: {
+ color: `${theme.palette.swatches.red[500]} !important`,
+ width: "40px",
+ height: "40px",
+ margin: "9px !important",
+ },
}));
export default function Home() {
@@ -25,39 +78,119 @@ export default function Home() {
const dispatch = useDispatch();
const mission = useSelector((state) => state.core.mission);
- const versions = useSelector((state) => state.home.versions);
+ const configuration = useSelector((state) => state.core.configuration);
- useEffect(() => {
+ const queryVersions = () => {
if (mission != null)
calls.api(
"versions",
{ mission: mission },
(res) => {
+ const v = res?.versions || [];
+ if (v.length > 0) v[v.length - 1].current = true;
dispatch(setVersions(res?.versions || []));
},
(res) => {
dispatch(
setSnackBarText({
- text: res?.message || "Failed to get history for mission.",
+ text:
+ res?.message || "Failed to get the history for the mission.",
severity: "error",
})
);
}
);
- }, [dispatch, mission]);
+ };
+
+ const handleExport = () => {
+ downloadObject(configuration, `${mission}_WORKING_config`, ".json");
+ dispatch(
+ setSnackBarText({
+ text: "Successfully exported working Configuration JSON.",
+ severity: "success",
+ })
+ );
+ };
+ const handleUpload = () => {
+ dispatch(
+ setModal({
+ name: "uploadConfig",
+ })
+ );
+ };
+ const handleClone = () => {
+ dispatch(
+ setModal({
+ name: "cloneConfig",
+ })
+ );
+ };
+ const handleDelete = () => {
+ dispatch(
+ setModal({
+ name: "deleteConfig",
+ })
+ );
+ };
return (
-
-
- {versions.map((v) => {
- return (
- -
- {v.version} - {v.createdAt}
-
- );
- })}
-
-
-
+ <>
+
+
+
{mission}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/configure/src/components/Tabs/Home/Modals/CloneConfigModal/CloneConfigModal.js b/configure/src/components/Tabs/Home/Modals/CloneConfigModal/CloneConfigModal.js
new file mode 100644
index 00000000..ec3f282d
--- /dev/null
+++ b/configure/src/components/Tabs/Home/Modals/CloneConfigModal/CloneConfigModal.js
@@ -0,0 +1,321 @@
+import React, { useState } from "react";
+import { useSelector, useDispatch } from "react-redux";
+
+import { calls } from "../../../../../core/calls";
+
+import {
+ setModal,
+ setSnackBarText,
+ setMissions,
+} from "../../../../../core/ConfigureStore";
+
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import Dialog from "@mui/material/Dialog";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
+import IconButton from "@mui/material/IconButton";
+import FormGroup from "@mui/material/FormGroup";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Checkbox from "@mui/material/Checkbox";
+
+import CloseSharpIcon from "@mui/icons-material/CloseSharp";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+
+import TextField from "@mui/material/TextField";
+
+import { makeStyles, useTheme } from "@mui/styles";
+import useMediaQuery from "@mui/material/useMediaQuery";
+
+const useStyles = makeStyles((theme) => ({
+ Modal: {
+ margin: theme.headHeights[1],
+ [theme.breakpoints.down("xs")]: {
+ margin: "6px",
+ },
+ "& .MuiDialog-container": {
+ height: "unset !important",
+ transform: "translateX(-50%) translateY(-50%)",
+ left: "50%",
+ top: "50%",
+ position: "absolute",
+ },
+ },
+ contents: {
+ background: theme.palette.primary.main,
+ height: "100%",
+ width: "500px",
+ },
+ heading: {
+ height: theme.headHeights[2],
+ boxSizing: "border-box",
+ background: theme.palette.swatches.p[0],
+ borderBottom: `1px solid ${theme.palette.swatches.grey[800]}`,
+ padding: `4px ${theme.spacing(2)} 4px ${theme.spacing(4)} !important`,
+ },
+ title: {
+ padding: `8px 0px`,
+ fontSize: theme.typography.pxToRem(16),
+ fontWeight: "bold",
+ color: theme.palette.swatches.grey[0],
+ textTransform: "uppercase",
+ },
+ content: {
+ padding: "8px 16px 16px 16px !important",
+ height: `calc(100% - ${theme.headHeights[2]}px)`,
+ },
+ closeIcon: {
+ padding: theme.spacing(1.5),
+ height: "100%",
+ margin: "4px 0px",
+ },
+ flexBetween: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ subtitle: {
+ fontSize: "14px !important",
+ width: "100%",
+ marginBottom: "8px !important",
+ color: theme.palette.swatches.grey[300],
+ letterSpacing: "0.2px",
+ },
+ subtitle2: {
+ fontSize: "12px !important",
+ fontStyle: "italic",
+ width: "100%",
+ marginBottom: "8px !important",
+ color: theme.palette.swatches.grey[400],
+ },
+ confirmInput: {
+ width: "100%",
+ margin: "10px 0px 4px 0px !important",
+ borderTop: `1px solid ${theme.palette.swatches.grey[500]}`,
+ },
+ backgroundIcon: {
+ margin: "7px 8px 0px 0px",
+ },
+
+ layerName: {
+ textAlign: "center",
+ fontSize: "24px !important",
+ letterSpacing: "1px !important",
+ color: theme.palette.swatches.grey[150],
+ fontWeight: "bold !important",
+ margin: "10px !important",
+ borderBottom: `1px solid ${theme.palette.swatches.grey[100]}`,
+ paddingBottom: "10px",
+ },
+ hasOccurrencesTitle: {
+ margin: "10px",
+ display: "flex",
+ },
+ hasOccurrences: {
+ fontStyle: "italic",
+ },
+ mission: {
+ background: theme.palette.swatches.p[11],
+ color: theme.palette.swatches.grey[900],
+ height: "24px",
+ lineHeight: "24px",
+ padding: "0px 5px",
+ borderRadius: "3px",
+ display: "inline-block",
+ letterSpacing: "1px",
+ marginLeft: "20px",
+ },
+ pathName: {
+ display: "flex",
+ marginLeft: "40px",
+ marginTop: "4px",
+ height: "24px",
+ lineHeight: "24px",
+ },
+ path: {
+ color: theme.palette.swatches.grey[500],
+ },
+ name: {
+ color: theme.palette.swatches.grey[100],
+ fontWeight: "bold",
+ },
+ confirmMessage: {
+ fontStyle: "italic",
+ fontSize: "15px !important",
+ },
+ dialogActions: {
+ display: "flex !important",
+ justifyContent: "space-between !important",
+ },
+ submit: {
+ background: `${theme.palette.swatches.p[0]} !important`,
+ color: `${theme.palette.swatches.grey[1000]} !important`,
+ "&:hover": {
+ background: `${theme.palette.swatches.grey[0]} !important`,
+ },
+ },
+ cancel: {},
+}));
+
+const MODAL_NAME = "cloneConfig";
+const CloneConfigModal = (props) => {
+ const { queryGeoDatasets } = props;
+ const c = useStyles();
+
+ const modal = useSelector((state) => state.core.modal[MODAL_NAME]);
+ const mission = useSelector((state) => state.core.mission);
+ const missions = useSelector((state) => state.core.missions);
+
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down("md"));
+
+ const dispatch = useDispatch();
+
+ const [newMissionName, setNewMissionName] = useState(null);
+ const [hasPaths, setHasPaths] = useState(false);
+
+ const handleClose = () => {
+ // close modal
+ dispatch(setModal({ name: MODAL_NAME, on: false }));
+ };
+ const handleSubmit = () => {
+ if (newMissionName == "" || newMissionName == null) {
+ dispatch(
+ setSnackBarText({
+ text: "A new mission name to clone into needs to be specified.",
+ severity: "error",
+ })
+ );
+ return;
+ }
+
+ for (let i = 0; i < missions.length; i++) {
+ if (newMissionName.toLowerCase() === missions[i].toLowerCase()) {
+ dispatch(
+ setSnackBarText({
+ text: "Must clone into a new mission name!",
+ severity: "error",
+ })
+ );
+ return;
+ }
+ }
+
+ calls.api(
+ "clone",
+ {
+ existingMission: mission,
+ cloneMission: newMissionName,
+ hasPaths: hasPaths,
+ },
+ (res) => {
+ dispatch(
+ setSnackBarText({
+ text: `Successfully cloned this mission into '${newMissionName}'.`,
+ severity: "success",
+ })
+ );
+ calls.api(
+ "missions",
+ null,
+ (res) => {
+ dispatch(setMissions(res.missions));
+ },
+ (res) => {
+ dispatch(
+ setSnackBarText({
+ text: res?.message || "Failed to get available missions.",
+ severity: "error",
+ })
+ );
+ }
+ );
+ handleClose();
+ },
+ (res) => {
+ dispatch(
+ setSnackBarText({
+ text: `Failed to clone this mission.`,
+ severity: "error",
+ })
+ );
+ }
+ );
+ };
+
+ return (
+
+ );
+};
+
+export default CloneConfigModal;
diff --git a/configure/src/pages/WebHooks/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js b/configure/src/components/Tabs/Home/Modals/DeleteConfigModal/DeleteConfigModal.js
similarity index 70%
rename from configure/src/pages/WebHooks/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js
rename to configure/src/components/Tabs/Home/Modals/DeleteConfigModal/DeleteConfigModal.js
index e77ed875..276e0b2e 100644
--- a/configure/src/pages/WebHooks/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js
+++ b/configure/src/components/Tabs/Home/Modals/DeleteConfigModal/DeleteConfigModal.js
@@ -1,9 +1,14 @@
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
-import { calls } from "../../../../core/calls";
+import { calls } from "../../../../../core/calls";
-import { setModal, setSnackBarText } from "../../../../core/ConfigureStore";
+import {
+ setModal,
+ setMission,
+ setMissions,
+ setSnackBarText,
+} from "../../../../../core/ConfigureStore";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
@@ -72,7 +77,6 @@ const useStyles = makeStyles((theme) => ({
subtitle: {
fontSize: "14px !important",
width: "100%",
- textAlign: "right",
marginBottom: "8px !important",
color: theme.palette.swatches.grey[300],
letterSpacing: "0.2px",
@@ -162,40 +166,40 @@ const useStyles = makeStyles((theme) => ({
cancel: {},
}));
-const MODAL_NAME = "deleteGeoDataset";
-const DeleteGeoDatasetModal = (props) => {
+const MODAL_NAME = "deleteConfig";
+const DeleteConfigModal = (props) => {
const { queryGeoDatasets } = props;
const c = useStyles();
const modal = useSelector((state) => state.core.modal[MODAL_NAME]);
- const geodatasets = useSelector((state) => state.core.geodatasets);
+ const mission = useSelector((state) => state.core.mission);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const dispatch = useDispatch();
- const [geoDatasetName, setGeoDatasetName] = useState(null);
+ const [missionName, setMissionName] = useState(null);
const handleClose = () => {
// close modal
dispatch(setModal({ name: MODAL_NAME, on: false }));
};
const handleSubmit = () => {
- if (!modal?.geoDataset?.name) {
+ if (!mission) {
dispatch(
setSnackBarText({
- text: "Cannot delete undefined GeoDataset.",
+ text: "Cannot delete undefined Mission.",
severity: "error",
})
);
return;
}
- if (geoDatasetName !== modal.geoDataset.name) {
+ if (missionName !== mission) {
dispatch(
setSnackBarText({
- text: "Confirmation GeoDataset name does not match.",
+ text: "Confirmation Mission name does not match.",
severity: "error",
})
);
@@ -203,35 +207,39 @@ const DeleteGeoDatasetModal = (props) => {
}
calls.api(
- "geodatasets_remove",
+ "destroy",
{
- urlReplacements: {
- name: modal.geoDataset.name,
- },
+ mission: mission,
},
(res) => {
- if (res.status === "success") {
- dispatch(
- setSnackBarText({
- text: `Successfully deleted the '${modal.geoDataset.name}' GeoDataset.`,
- severity: "success",
- })
- );
- queryGeoDatasets();
- handleClose();
- } else {
- dispatch(
- setSnackBarText({
- text: `Failed to delete the '${modal.geoDataset.name}' GeoDataset.`,
- severity: "error",
- })
- );
- }
+ dispatch(
+ setSnackBarText({
+ text: `Successfully deleted the '${mission}' Mission.`,
+ severity: "success",
+ })
+ );
+ dispatch(setMission(null));
+ calls.api(
+ "missions",
+ null,
+ (res) => {
+ dispatch(setMissions(res.missions));
+ },
+ (res) => {
+ dispatch(
+ setSnackBarText({
+ text: res?.message || "Failed to get available missions.",
+ severity: "error",
+ })
+ );
+ }
+ );
+ handleClose();
},
(res) => {
dispatch(
setSnackBarText({
- text: `Failed to delete the '${modal.geoDataset.name}' GeoDataset.`,
+ text: `Failed to delete the '${mission}' Mission.`,
severity: "error",
})
);
@@ -239,30 +247,6 @@ const DeleteGeoDatasetModal = (props) => {
);
};
- let occurrences = [];
-
- if (modal?.geoDataset?.occurrences)
- occurrences = Object.keys(modal?.geoDataset?.occurrences)
- .map((mission) => {
- const m = modal?.geoDataset?.occurrences[mission];
- if (m.length == 0) return null;
- else {
- const items = [{mission}
];
- m.forEach((n) => {
- items.push(
-
-
- {`${n.path}.`.replaceAll(".", " ➔ ")}
-
-
{n.name}
-
- );
- });
- return items;
- }
- })
- .filter(Boolean);
-
return (