diff --git a/ui/src/components/CreateEditVdbeForm.jsx b/ui/src/components/CreateEditVdbeForm.jsx
new file mode 100644
index 00000000..8c58fefe
--- /dev/null
+++ b/ui/src/components/CreateEditVdbeForm.jsx
@@ -0,0 +1,139 @@
+import { useState } from "react";
+import InsetPanel from "./InsetPanel";
+import CheckCircleOutlineRoundedIcon from "@mui/icons-material/CheckCircleOutlineRounded";
+import FormControl from "@mui/material/FormControl";
+import InputLabel from "@mui/material/InputLabel";
+import TextField from "@mui/material/TextField";
+import MenuItem from "@mui/material/MenuItem";
+import InputAdornment from "@mui/material/InputAdornment";
+import Button from "@mui/material/Button";
+import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
+import EditRoundedIcon from "@mui/icons-material/EditRounded";
+import Select from "@mui/material/Select";
+import VdbeView from "./VdbeView";
+import "./styles/CreateEditVdbeForm.css";
+
+function CreateEditFormFields({ vdbe, setVdbe }) {
+ const onStalenessChange = (event) => {
+ const maxStalenessMins = parseInt(event.target.value);
+ if (isNaN(maxStalenessMins)) {
+ setVdbe({ ...vdbe, max_staleness_ms: null });
+ return;
+ }
+ setVdbe({ ...vdbe, max_staleness_ms: maxStalenessMins * 60 * 1000 });
+ };
+
+ const onSloChange = (event) => {
+ const sloMs = parseInt(event.target.value);
+ if (isNaN(sloMs)) {
+ setVdbe({ ...vdbe, p90_latency_slo_ms: null });
+ return;
+ }
+ setVdbe({ ...vdbe, p90_latency_slo_ms: sloMs });
+ };
+
+ return (
+
+ setVdbe({ ...vdbe, name: event.target.value })}
+ />
+ minutes
+ ),
+ },
+ }}
+ value={
+ vdbe.max_staleness_ms != null ? vdbe.max_staleness_ms / 60000 : ""
+ }
+ onChange={onStalenessChange}
+ />
+ milliseconds
+ ),
+ },
+ }}
+ value={vdbe.p90_latency_slo_ms != null ? vdbe.p90_latency_slo_ms : ""}
+ onChange={onSloChange}
+ />
+
+ Query Interface
+
+
+
+ );
+}
+
+function getEmptyVdbe() {
+ return {
+ name: null,
+ max_staleness_ms: null,
+ p90_latency_slo_ms: null,
+ queryInterface: "postgresql",
+ tables: [],
+ };
+}
+
+function CreateEditVdbeForm({ isEdit, currentVdbe }) {
+ const [vdbe, setVdbe] = useState(
+ currentVdbe != null ? currentVdbe : getEmptyVdbe(),
+ );
+
+ return (
+
+
+ {isEdit ? (
+
+ ) : (
+
+ )}
+ {isEdit ? "Edit VDBE" : "Create VDBE"}
+
+
+
+
+ }
+ >
+ {isEdit ? "Save" : "Create"}
+
+
+
+ );
+}
+
+export default CreateEditVdbeForm;
diff --git a/ui/src/components/InsetPanel.jsx b/ui/src/components/InsetPanel.jsx
new file mode 100644
index 00000000..78c183de
--- /dev/null
+++ b/ui/src/components/InsetPanel.jsx
@@ -0,0 +1,7 @@
+import "./styles/InsetPanel.css";
+
+function InsetPanel({ children, className }) {
+ return {children}
;
+}
+
+export default InsetPanel;
diff --git a/ui/src/components/VdbeView.jsx b/ui/src/components/VdbeView.jsx
index 2acc3958..0f7e6a68 100644
--- a/ui/src/components/VdbeView.jsx
+++ b/ui/src/components/VdbeView.jsx
@@ -13,6 +13,10 @@ import {
} from "../highlight";
function formatMilliseconds(milliseconds) {
+ if (milliseconds == null) {
+ return null;
+ }
+
const precision = 2;
if (milliseconds >= 1000 * 60 * 60) {
// Use hours.
@@ -27,6 +31,10 @@ function formatMilliseconds(milliseconds) {
}
function formatFreshness(maxStalenessMs) {
+ if (maxStalenessMs == null) {
+ return null;
+ }
+
if (maxStalenessMs === 0) {
return "No staleness";
}
@@ -34,12 +42,19 @@ function formatFreshness(maxStalenessMs) {
}
function formatDialect(queryInterface) {
+ if (queryInterface == null) {
+ return null;
+ }
+
if (queryInterface === "postgresql") {
return "PostgreSQL SQL";
} else if (queryInterface === "athena") {
return "Athena SQL";
} else if (queryInterface === "common") {
return "SQL-99";
+ } else {
+ console.error("Unknown", queryInterface);
+ return null;
}
}
@@ -60,7 +75,13 @@ function EditControls({ onEditClick, onDeleteClick }) {
);
}
-function VdbeView({ vdbe, highlight, onTableHoverEnter, onTableHoverExit }) {
+function VdbeView({
+ vdbe,
+ highlight,
+ onTableHoverEnter,
+ onTableHoverExit,
+ editable,
+}) {
const vengName = vdbe.name;
const tables = vdbe.tables;
const freshness = formatFreshness(vdbe.max_staleness_ms);
@@ -74,13 +95,20 @@ function VdbeView({ vdbe, highlight, onTableHoverEnter, onTableHoverExit }) {
>
{vengName}
- {}} onDeleteClick={() => {}} />
+ {editable && (
+ {}} onDeleteClick={() => {}} />
+ )}
- - 🌿: {freshness}
- - ⏱️: p90 Query Latency ≤ {peakLatency}
- - 🗣: {dialect}
+ - 🌿: {freshness != null ? freshness : "-----"}
+ -
+ ⏱️:{" "}
+ {peakLatency != null
+ ? `p90 Query Latency ≤ ${peakLatency}`
+ : "-----"}
+
+ - 🗣: {dialect != null ? dialect : "-----"}
diff --git a/ui/src/components/VirtualInfraView.jsx b/ui/src/components/VirtualInfraView.jsx
index ba53b255..974d8442 100644
--- a/ui/src/components/VirtualInfraView.jsx
+++ b/ui/src/components/VirtualInfraView.jsx
@@ -69,6 +69,7 @@ function VirtualInfraView({
onTableHoverEnter={onTableHoverEnter}
onTableHoverExit={onTableHoverExit}
vdbe={vdbe}
+ editable={true}
/>
))}
diff --git a/ui/src/components/styles/CreateEditVdbeForm.css b/ui/src/components/styles/CreateEditVdbeForm.css
new file mode 100644
index 00000000..159cd283
--- /dev/null
+++ b/ui/src/components/styles/CreateEditVdbeForm.css
@@ -0,0 +1,42 @@
+.create-edit-vdbe-form {
+ display: flex;
+ flex-direction: column;
+}
+
+.create-edit-vdbe-form h2 {
+ margin-bottom: 20px;
+}
+
+.cev-form-body {
+ display: flex;
+ flex-direction: row;
+}
+
+.cev-form-fields {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 3;
+ margin-bottom: 10px;
+}
+
+.cev-form-fields .cev-field {
+ margin: 0 0 15px 0;
+}
+
+.cev-preview {
+ flex-grow: 2;
+ display: flex;
+ justify-content: center;
+ margin-top: -20px;
+}
+
+.cev-buttons {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.cev-buttons button {
+ margin-left: 20px;
+}
diff --git a/ui/src/components/styles/InsetPanel.css b/ui/src/components/styles/InsetPanel.css
new file mode 100644
index 00000000..891855b2
--- /dev/null
+++ b/ui/src/components/styles/InsetPanel.css
@@ -0,0 +1,15 @@
+.inset-panel-wrap {
+ display: flex;
+ flex-direction: column;
+ background-color: #f7f8f8;
+ border-radius: 19px;
+ padding: 29px;
+ margin-bottom: 19px;
+}
+
+.inset-panel-wrap h2 {
+ display: flex;
+ align-items: center;
+ font-size: 1.5em;
+ margin: 0 0 5px 0;
+}