From 6b25309aed51b575464a719dc271dad661e3cf5f Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 19 May 2024 01:51:09 -0700 Subject: [PATCH] variables working --- bin/core/src/api/write/variable.rs | 4 + .../config/components/environment.tsx | 133 +++++++++-- frontend/src/components/sidebar.tsx | 7 + frontend/src/components/topbar.tsx | 9 + frontend/src/components/updates/details.tsx | 205 +++++++++-------- frontend/src/components/updates/topbar.tsx | 4 +- frontend/src/components/util.tsx | 9 +- frontend/src/lib/socket.tsx | 9 +- frontend/src/pages/alerts.tsx | 12 +- frontend/src/pages/tags.tsx | 4 +- frontend/src/pages/updates.tsx | 51 ++++- frontend/src/pages/variables.tsx | 212 ++++++++++++++++++ frontend/src/router.tsx | 6 +- 13 files changed, 535 insertions(+), 130 deletions(-) create mode 100644 frontend/src/pages/variables.tsx diff --git a/bin/core/src/api/write/variable.rs b/bin/core/src/api/write/variable.rs index 87ddb8328..47cf196af 100644 --- a/bin/core/src/api/write/variable.rs +++ b/bin/core/src/api/write/variable.rs @@ -76,6 +76,10 @@ impl Resolve for State { let variable = get_variable(&name).await?; + if value == variable.value { + return Err(anyhow!("no change")); + } + db_client() .await .variables diff --git a/frontend/src/components/resources/deployment/config/components/environment.tsx b/frontend/src/components/resources/deployment/config/components/environment.tsx index 600b673ae..75d1afd82 100644 --- a/frontend/src/components/resources/deployment/config/components/environment.tsx +++ b/frontend/src/components/resources/deployment/config/components/environment.tsx @@ -3,6 +3,7 @@ import { useRead } from "@lib/hooks"; import { env_to_text, text_to_env } from "@lib/utils"; import { Types } from "@monitor/client"; import { Button } from "@ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs"; import { Textarea } from "@ui/textarea"; import { RefObject, createRef, useEffect, useState } from "react"; @@ -53,29 +54,117 @@ const Secrets = ({ /// eg server id server: string; }) => { - const secrets = useRead("GetAvailableSecrets", { server }).data; + const { variables, secrets: core_secrets } = useRead("ListVariables", {}) + .data ?? { + variables: [], + secrets: [], + }; + const periphery_secrets = + useRead("GetAvailableSecrets", { server }).data || []; + + // Get unique list of secrets between core and periphery + const secrets = [...new Set([...core_secrets, ...periphery_secrets])]; + const _env = env || ""; - return ( - secrets && - secrets.length > 0 && ( -
-
secrets:
- {secrets?.map((secret) => ( - - ))} + + if (variables.length === 0 && secrets.length === 0) return; + + if (variables.length === 0) { + // ONLY SECRETS + return ( +
+

Secrets

+
+ {secrets.map((secret) => ( + + ))} +
- ) + ); + } + + if (secrets.length === 0) { + // ONLY VARIABLES + return ( +
+

Variables

+
+ {variables.map(({ name }) => ( + + ))} +
+
+ ); + } + + return ( + + + Variables + Secrets + + +
+ {variables.map(({ name }) => ( + + ))} +
+
+ +
+ {secrets.map((secret) => ( + + ))} +
+
+
); }; diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index d1e523217..523e12164 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -9,6 +9,7 @@ import { FolderTree, Tag, UserCircle2, + Variable, } from "lucide-react"; import { Link, useLocation } from "react-router-dom"; import { ResourceComponents } from "./resources"; @@ -74,6 +75,12 @@ export const Sidebar = () => { icon={} /> + } + /> + { ? [, "Api Keys"] : location.pathname === "/tags" ? [, "Tags"] + : location.pathname === "/variables" + ? [, "Variables"] : location.pathname === "/alerts" ? [, "Alerts"] : location.pathname === "/updates" @@ -164,6 +167,12 @@ const PrimaryDropdown = () => { to="/updates" /> + } + to="/variables" + /> + } diff --git a/frontend/src/components/updates/details.tsx b/frontend/src/components/updates/details.tsx index ca162e4e3..1c2d7558b 100644 --- a/frontend/src/components/updates/details.tsx +++ b/frontend/src/components/updates/details.tsx @@ -6,7 +6,7 @@ import { SheetTitle, SheetTrigger, } from "@ui/sheet"; -import { Calendar, Clock, Milestone, User } from "lucide-react"; +import { Calendar, Clock, Loader2, Milestone, User } from "lucide-react"; import { Card, CardContent, @@ -133,16 +133,6 @@ export const UpdateDetailsInner = ({ open: boolean; setOpen: React.Dispatch>; }) => { - const update = useRead("GetUpdate", { id }).data; - if (!update) return null; - - const Components = - update.target.type === "System" - ? null - : ResourceComponents[update.target.type]; - - if (!Components) return null; - return ( {children} @@ -151,17 +141,44 @@ export const UpdateDetailsInner = ({ side="top" onClick={() => setOpen(false)} > - - - {update.operation - .split("_") - .map((s) => s[0].toUpperCase() + s.slice(1)) - .join(" ")}{" "} - {!version_is_none(update.version) && fmt_version(update.version)} - - - -
+ {open && } + + + ); +}; + +const UpdateDetailsContent = ({ + id, + setOpen, +}: { + id: string; + setOpen: React.Dispatch>; +}) => { + const update = useRead("GetUpdate", { id }).data; + if (!update) + return ( +
+ +
+ ); + const Components = + update.target.type === "System" + ? null + : ResourceComponents[update.target.type]; + return ( + <> + + + {update.operation + .split("_") + .map((s) => s[0].toUpperCase() + s.slice(1)) + .join(" ")}{" "} + {!version_is_none(update.version) && fmt_version(update.version)} + + + +
+ {Components && (
- {update.version && ( -
- - {fmt_version(update.version)} + )} + {update.version && ( +
+ + {fmt_version(update.version)} +
+ )} +
+
+
+ + {new Date(update.start_ts).toLocaleString()} +
+
+ + {update.end_ts + ? fmt_duration(update.start_ts, update.end_ts) + : "ongoing"} +
+
+
+
+
+ {update.logs?.map((log, i) => ( + + + {log.stage} + + + Stage {i + 1} of {update.logs.length} + + | + + + {fmt_duration(log.start_ts, log.end_ts)} + + + + + {log.command && ( +
+ command +
+                    {log.command}
+                  
)} -
-
-
- - {new Date(update.start_ts).toLocaleString()} -
-
- - {update.end_ts - ? fmt_duration(update.start_ts, update.end_ts) - : "ongoing"} -
-
- - -
- {update.logs?.map((log, i) => ( - - - {log.stage} - - - Stage {i + 1} of {update.logs.length} - - | - - - {fmt_duration(log.start_ts, log.end_ts)} - - - - - {log.command && ( -
- command -
-                      {log.command}
-                    
-
- )} - {log.stdout && ( -
- stdout -
-                  
- )} - {log.stderr && ( -
- stderr -
-                  
- )} -
-
- ))} -
- - + {log.stdout && ( +
+ stdout +
+                
+ )} + {log.stderr && ( +
+ stderr +
+                
+ )} + + + ))} +
+ ); }; diff --git a/frontend/src/components/updates/topbar.tsx b/frontend/src/components/updates/topbar.tsx index e4870f411..7467e0174 100644 --- a/frontend/src/components/updates/topbar.tsx +++ b/frontend/src/components/updates/topbar.tsx @@ -5,7 +5,7 @@ import { DropdownMenuGroup, DropdownMenuTrigger, } from "@ui/dropdown-menu"; -import { Bell, Check, Circle, Loader2, X } from "lucide-react"; +import { Bell, Check, Circle, Loader2, Settings, X } from "lucide-react"; import { Button } from "@ui/button"; import { Calendar } from "lucide-react"; import { UpdateDetails, UpdateUser } from "./details"; @@ -80,6 +80,8 @@ const SingleUpdate = ({ update }: { update: Types.UpdateListItem }) => {
{Components && } {Components && } + {!Components && } + {!Components && "System"}
diff --git a/frontend/src/components/util.tsx b/frontend/src/components/util.tsx index f05cfed1a..571f2c972 100644 --- a/frontend/src/components/util.tsx +++ b/frontend/src/components/util.tsx @@ -309,6 +309,7 @@ export const TextUpdateMenu = ({ placeholder, confirmButton, disabled, + fullWidth, }: { title: string; value: string | undefined; @@ -317,6 +318,7 @@ export const TextUpdateMenu = ({ placeholder?: string; confirmButton?: boolean; disabled?: boolean; + fullWidth?: boolean; }) => { const [open, setOpen] = useState(false); const [_value, setValue] = useState(value); @@ -329,7 +331,12 @@ export const TextUpdateMenu = ({ return ( - +
ws.close(), 30_000); - return ws + return ws; }; diff --git a/frontend/src/pages/alerts.tsx b/frontend/src/pages/alerts.tsx index 91ff87301..8dd427f76 100644 --- a/frontend/src/pages/alerts.tsx +++ b/frontend/src/pages/alerts.tsx @@ -12,6 +12,7 @@ import { SelectValue, } from "@ui/select"; import { Switch } from "@ui/switch"; +import { AlertTriangle } from "lucide-react"; import { useState } from "react"; import { useParams } from "react-router"; @@ -48,8 +49,10 @@ export const Alerts = () => { page, }).data; return ( - -
+ } + actions={
{
- + } + > +
-
+ Page: {page + 1} + +
+
); }; @@ -54,12 +76,14 @@ const ResourceUpdates = ({ const name = useRead(`List${type}s`, {}).data?.find((r) => r.id === id)?.name; useSetTitle(name && `${name} | Updates`); const [operation, setOperation] = useState(); + const [page, setPage] = useState(0); const updates = useRead("ListUpdates", { query: { "target.type": type, "target.id": id, operation, }, + page, }).data; const Components = ResourceComponents[type]; return ( @@ -75,7 +99,26 @@ const ResourceUpdates = ({ /> } > - +
+ +
+ + Page: {page + 1} + +
+
); }; diff --git a/frontend/src/pages/variables.tsx b/frontend/src/pages/variables.tsx new file mode 100644 index 000000000..ebe0ef49d --- /dev/null +++ b/frontend/src/pages/variables.tsx @@ -0,0 +1,212 @@ +import { Page } from "@components/layouts"; +import { ConfirmButton, TextUpdateMenu } from "@components/util"; +import { + useInvalidate, + useRead, + useSetTitle, + useUser, + useWrite, +} from "@lib/hooks"; +import { Button } from "@ui/button"; +import { DataTable, SortableHeader } from "@ui/data-table"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@ui/dialog"; +import { Input } from "@ui/input"; +import { useToast } from "@ui/use-toast"; +import { Check, Loader2, PlusCircle, Trash, Variable } from "lucide-react"; +import { useState } from "react"; + +export const Variables = () => { + const user = useUser().data; + const disabled = !user?.admin; + useSetTitle("Variables"); + const [search, setSearch] = useState(""); + const { variables } = useRead("ListVariables", {}).data ?? { + variables: [], + secrets: [], + }; + const searchSplit = search?.toLowerCase().split(" ") || []; + const filtered = + variables?.filter((variable) => { + if (searchSplit.length > 0) { + const name = variable.name.toLowerCase(); + return searchSplit.every((search) => name.includes(search)); + } else return true; + }) ?? []; + const { toast } = useToast(); + const { mutate: updateValue } = useWrite("UpdateVariableValue"); + const inv = useInvalidate(); + const { mutate: updateDescription } = useWrite("UpdateVariableDescription", { + onSuccess: () => { + inv(["ListVariables"], ["GetVariable"]); + toast({ title: "Updated variable description" }); + }, + }); + return ( + } + actions={ +
+ setSearch(e.target.value)} + className="w-[200px] lg:w-[300px]" + /> + +
+ } + > + ( + + ), + }, + { + accessorKey: "value", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( + { + if (row.original.value === value) { + return; + } + updateValue({ name: row.original.name, value }); + }} + triggerClassName="w-full" + disabled={disabled} + fullWidth + /> + ); + }, + }, + { + accessorKey: "description", + header: "Description", + cell: ({ row }) => { + return ( + { + if (row.original.description === description) { + return; + } + updateDescription({ + name: row.original.name, + description, + }); + }} + triggerClassName="w-full" + disabled={disabled} + fullWidth + /> + ); + }, + }, + { + header: "Delete", + cell: ({ row }) => , + }, + ]} + /> +
+ ); +}; + +const CreateVariable = () => { + const { toast } = useToast(); + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const invalidate = useInvalidate(); + const { mutate, isPending } = useWrite("CreateVariable", { + onSuccess: () => { + invalidate(["ListVariables"], ["GetVariable"]); + toast({ title: "Variable Created" }); + setOpen(false); + }, + onError: (e) => { + console.log("create variable error:" + e); + toast({ title: "Failed to create variable" }); + setOpen(false); + }, + }); + const submit = () => mutate({ name }); + return ( + + + + + + + Create Variable + +
+
+ Name + + setName(e.target.value.toUpperCase().replaceAll(" ", "_")) + } + /> +
+
+ + + +
+
+ ); +}; + +const DeleteVariable = ({ name }: { name: string }) => { + const invalidate = useInvalidate(); + const { toast } = useToast(); + const { mutate, isPending } = useWrite("DeleteVariable", { + onSuccess: () => { + invalidate(["ListVariables"], ["GetVariable"]); + toast({ title: "Variable Deleted" }); + }, + onError: () => { + toast({ title: "Failed to delete variable" }); + }, + }); + return ( + } + onClick={() => mutate({ name })} + loading={isPending} + /> + ); +}; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 0b70f494c..3b966268a 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -16,6 +16,7 @@ import { ResourceStats } from "@pages/resource_stats"; import { Alerts } from "@pages/alerts"; import { UserPage } from "@pages/user"; import { UserGroupPage } from "@pages/user-group"; +import { Variables } from "@pages/variables"; const ROUTER = createBrowserRouter([ { @@ -26,9 +27,11 @@ const ROUTER = createBrowserRouter([ { path: "keys", element: }, { path: "tags", element: }, { path: "tree", element: }, - { path: "resources", element: }, { path: "alerts", element: }, { path: "updates", element: }, + { path: "variables", element: }, + { path: "resources", element: }, + { path: "user-groups/:id", element: }, { path: "users", children: [ @@ -36,7 +39,6 @@ const ROUTER = createBrowserRouter([ { path: ":id", element: }, ], }, - { path: "user-groups/:id", element: }, { path: ":type", children: [