forked from github-starred/komodo
variables working
This commit is contained in:
@@ -76,6 +76,10 @@ impl Resolve<UpdateVariableValue, User> for State {
|
||||
|
||||
let variable = get_variable(&name).await?;
|
||||
|
||||
if value == variable.value {
|
||||
return Err(anyhow!("no change"));
|
||||
}
|
||||
|
||||
db_client()
|
||||
.await
|
||||
.variables
|
||||
|
||||
@@ -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 && (
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="text-muted-foreground">secrets:</div>
|
||||
{secrets?.map((secret) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={secret}
|
||||
onClick={() =>
|
||||
setEnv(
|
||||
_env.slice(0, envRef.current?.selectionStart) +
|
||||
`[[${secret}]]` +
|
||||
_env.slice(envRef.current?.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{secret}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
if (variables.length === 0 && secrets.length === 0) return;
|
||||
|
||||
if (variables.length === 0) {
|
||||
// ONLY SECRETS
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<h2 className="text-muted-foreground">Secrets</h2>
|
||||
<div className="flex gap-4 items-center flex-wrap w-full">
|
||||
{secrets.map((secret) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={secret}
|
||||
onClick={() =>
|
||||
setEnv(
|
||||
_env.slice(0, envRef.current?.selectionStart) +
|
||||
`[[${secret}]]` +
|
||||
_env.slice(envRef.current?.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{secret}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (secrets.length === 0) {
|
||||
// ONLY VARIABLES
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<h2 className="text-muted-foreground">Variables</h2>
|
||||
<div className="flex gap-4 items-center flex-wrap w-full">
|
||||
{variables.map(({ name }) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={name}
|
||||
onClick={() =>
|
||||
setEnv(
|
||||
_env.slice(0, envRef.current?.selectionStart) +
|
||||
`[[${name}]]` +
|
||||
_env.slice(envRef.current?.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs className="w-full" defaultValue="Variables">
|
||||
<TabsList>
|
||||
<TabsTrigger value="Variables">Variables</TabsTrigger>
|
||||
<TabsTrigger value="Secrets">Secrets</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="Variables">
|
||||
<div className="flex gap-4 items-center w-full flex-wrap pt-1">
|
||||
{variables.map(({ name }) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={name}
|
||||
onClick={() =>
|
||||
setEnv(
|
||||
_env.slice(0, envRef.current?.selectionStart) +
|
||||
`[[${name}]]` +
|
||||
_env.slice(envRef.current?.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="Secrets">
|
||||
<div className="flex gap-4 items-center w-full flex-wrap pt-1">
|
||||
{secrets.map((secret) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={secret}
|
||||
onClick={() =>
|
||||
setEnv(
|
||||
_env.slice(0, envRef.current?.selectionStart) +
|
||||
`[[${secret}]]` +
|
||||
_env.slice(envRef.current?.selectionStart, undefined)
|
||||
)
|
||||
}
|
||||
>
|
||||
{secret}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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={<Bell className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Variables"
|
||||
to="/variables"
|
||||
icon={<Variable className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Tags"
|
||||
to="/tags"
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Tag,
|
||||
UserCircle2,
|
||||
Users,
|
||||
Variable,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -104,6 +105,8 @@ const PrimaryDropdown = () => {
|
||||
? [<Key className="w-4 h-4" />, "Api Keys"]
|
||||
: location.pathname === "/tags"
|
||||
? [<Tag className="w-4 h-4" />, "Tags"]
|
||||
: location.pathname === "/variables"
|
||||
? [<Variable className="w-4 h-4" />, "Variables"]
|
||||
: location.pathname === "/alerts"
|
||||
? [<AlertTriangle className="w-4 h-4" />, "Alerts"]
|
||||
: location.pathname === "/updates"
|
||||
@@ -164,6 +167,12 @@ const PrimaryDropdown = () => {
|
||||
to="/updates"
|
||||
/>
|
||||
|
||||
<DropdownLinkItem
|
||||
label="Variables"
|
||||
icon={<Variable className="w-4 h-4" />}
|
||||
to="/variables"
|
||||
/>
|
||||
|
||||
<DropdownLinkItem
|
||||
label="Tags"
|
||||
icon={<Tag className="w-4 h-4" />}
|
||||
|
||||
@@ -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<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
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 (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
@@ -151,17 +141,44 @@ export const UpdateDetailsInner = ({
|
||||
side="top"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<SheetHeader className="mb-4">
|
||||
<SheetTitle>
|
||||
{update.operation
|
||||
.split("_")
|
||||
.map((s) => s[0].toUpperCase() + s.slice(1))
|
||||
.join(" ")}{" "}
|
||||
{!version_is_none(update.version) && fmt_version(update.version)}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="flex flex-col gap-2">
|
||||
<UpdateUser user_id={update.operator} />
|
||||
<div className="flex gap-4">
|
||||
{open && <UpdateDetailsContent id={id} setOpen={setOpen} />}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
const UpdateDetailsContent = ({
|
||||
id,
|
||||
setOpen,
|
||||
}: {
|
||||
id: string;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const update = useRead("GetUpdate", { id }).data;
|
||||
if (!update)
|
||||
return (
|
||||
<div className="w-full flex justify-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
const Components =
|
||||
update.target.type === "System"
|
||||
? null
|
||||
: ResourceComponents[update.target.type];
|
||||
return (
|
||||
<>
|
||||
<SheetHeader className="mb-4">
|
||||
<SheetTitle>
|
||||
{update.operation
|
||||
.split("_")
|
||||
.map((s) => s[0].toUpperCase() + s.slice(1))
|
||||
.join(" ")}{" "}
|
||||
{!version_is_none(update.version) && fmt_version(update.version)}
|
||||
</SheetTitle>
|
||||
<SheetDescription className="flex flex-col gap-2">
|
||||
<UpdateUser user_id={update.operator} />
|
||||
<div className="flex gap-4">
|
||||
{Components && (
|
||||
<Link
|
||||
to={`/${usableResourcePath(
|
||||
update.target.type as UsableResource
|
||||
@@ -175,79 +192,79 @@ export const UpdateDetailsInner = ({
|
||||
<Components.Name id={update.target.id} />
|
||||
</div>
|
||||
</Link>
|
||||
{update.version && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Milestone className="w-4 h-4" />
|
||||
{fmt_version(update.version)}
|
||||
)}
|
||||
{update.version && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Milestone className="w-4 h-4" />
|
||||
{fmt_version(update.version)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{new Date(update.start_ts).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
{update.end_ts
|
||||
? fmt_duration(update.start_ts, update.end_ts)
|
||||
: "ongoing"}
|
||||
</div>
|
||||
</div>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="grid gap-2">
|
||||
{update.logs?.map((log, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="flex-col">
|
||||
<CardTitle>{log.stage}</CardTitle>
|
||||
<CardDescription className="flex gap-2">
|
||||
<span>
|
||||
Stage {i + 1} of {update.logs.length}
|
||||
</span>
|
||||
<span>|</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
{fmt_duration(log.start_ts, log.end_ts)}
|
||||
</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{log.command && (
|
||||
<div>
|
||||
<CardDescription>command</CardDescription>
|
||||
<pre className="max-h-[500px] overflow-y-auto">
|
||||
{log.command}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{new Date(update.start_ts).toLocaleString()}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
{update.end_ts
|
||||
? fmt_duration(update.start_ts, update.end_ts)
|
||||
: "ongoing"}
|
||||
</div>
|
||||
</div>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="grid gap-2">
|
||||
{update.logs?.map((log, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="flex-col">
|
||||
<CardTitle>{log.stage}</CardTitle>
|
||||
<CardDescription className="flex gap-2">
|
||||
<span>
|
||||
Stage {i + 1} of {update.logs.length}
|
||||
</span>
|
||||
<span>|</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
{fmt_duration(log.start_ts, log.end_ts)}
|
||||
</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{log.command && (
|
||||
<div>
|
||||
<CardDescription>command</CardDescription>
|
||||
<pre className="max-h-[500px] overflow-y-auto">
|
||||
{log.command}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{log.stdout && (
|
||||
<div>
|
||||
<CardDescription>stdout</CardDescription>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(log.stdout),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{log.stderr && (
|
||||
<div>
|
||||
<CardDescription>stderr</CardDescription>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(log.stderr),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
{log.stdout && (
|
||||
<div>
|
||||
<CardDescription>stdout</CardDescription>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(log.stdout),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{log.stderr && (
|
||||
<div>
|
||||
<CardDescription>stderr</CardDescription>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeOnlySpan(log.stderr),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }) => {
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
{Components && <Components.Icon />}
|
||||
{Components && <Components.Name id={update.target.id} />}
|
||||
{!Components && <Settings className="w-4 h-4" />}
|
||||
{!Components && "System"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground w-48">
|
||||
|
||||
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-fit">
|
||||
<Card
|
||||
className={cn(
|
||||
"px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer",
|
||||
fullWidth ? "w-full" : "w-fit"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm text-nowrap overflow-hidden overflow-ellipsis",
|
||||
|
||||
@@ -100,6 +100,13 @@ const on_message = (
|
||||
["GetServerTemplatesSummary"]
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
update.target.type === "System" &&
|
||||
update.operation.includes("Variable")
|
||||
) {
|
||||
invalidate(["ListVariables"], ["GetVariable"]);
|
||||
}
|
||||
};
|
||||
|
||||
export const WebsocketProvider = ({
|
||||
@@ -188,5 +195,5 @@ const make_websocket = (
|
||||
// force close every 30s to trigger reconnect and keep fresh
|
||||
setTimeout(() => ws.close(), 30_000);
|
||||
|
||||
return ws
|
||||
return ws;
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<Page title="Alerts">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Page
|
||||
title="Alerts"
|
||||
icon={<AlertTriangle className="w-8 h-8" />}
|
||||
actions={
|
||||
<div className="flex gap-4 items-center justify-end">
|
||||
<div
|
||||
className="flex gap-3 items-center cursor-pointer"
|
||||
@@ -76,9 +79,10 @@ export const Alerts = () => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<AlertsTable alerts={alerts?.alerts ?? []} showResolved />
|
||||
|
||||
<div className="flex gap-4 justify-center items-center text-muted-foreground">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Button } from "@ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { Trash, PlusCircle, Loader2, Check } from "lucide-react";
|
||||
import { Trash, PlusCircle, Loader2, Check, Tag } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@ui/input";
|
||||
import { UpdateUser } from "@components/updates/details";
|
||||
@@ -28,12 +28,14 @@ export const Tags = () => {
|
||||
return (
|
||||
<Page
|
||||
title="Tags"
|
||||
icon={<Tag className="w-8 h-8" />}
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
placeholder="search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
<CreateTag />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRead, useResourceParamType, useSetTitle } from "@lib/hooks";
|
||||
import { Types } from "@monitor/client";
|
||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||
import { UsableResource } from "@types";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
CommandList,
|
||||
} from "@ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { SearchX } from "lucide-react";
|
||||
import { Bell, SearchX } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
@@ -31,15 +32,36 @@ export const Updates = () => {
|
||||
const AllUpdates = () => {
|
||||
useSetTitle("Updates");
|
||||
const [operation, setOperation] = useState<Types.Operation | undefined>();
|
||||
const updates = useRead("ListUpdates", { query: { operation } }).data;
|
||||
const [page, setPage] = useState(0);
|
||||
const updates = useRead("ListUpdates", { query: { operation }, page }).data;
|
||||
return (
|
||||
<Page
|
||||
title="Updates"
|
||||
icon={<Bell className="w-8 h-8" />}
|
||||
actions={
|
||||
<OperationSelector selected={operation} onSelect={setOperation} />
|
||||
}
|
||||
>
|
||||
<UpdatesTable updates={updates?.updates ?? []} showTarget />
|
||||
<div className="flex flex-col gap-4">
|
||||
<UpdatesTable updates={updates?.updates ?? []} showTarget />
|
||||
<div className="flex gap-4 justify-center items-center text-muted-foreground">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 0}
|
||||
>
|
||||
Prev Page
|
||||
</Button>
|
||||
Page: {page + 1}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => updates?.next_page && setPage(updates.next_page)}
|
||||
disabled={!updates?.next_page}
|
||||
>
|
||||
Next Page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -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<Types.Operation | undefined>();
|
||||
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 = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<UpdatesTable updates={updates?.updates ?? []} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<UpdatesTable updates={updates?.updates ?? []} />
|
||||
<div className="flex gap-4 justify-center items-center text-muted-foreground">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={page === 0}
|
||||
>
|
||||
Prev Page
|
||||
</Button>
|
||||
Page: {page + 1}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => updates?.next_page && setPage(updates.next_page)}
|
||||
disabled={!updates?.next_page}
|
||||
>
|
||||
Next Page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
212
frontend/src/pages/variables.tsx
Normal file
212
frontend/src/pages/variables.tsx
Normal file
@@ -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 (
|
||||
<Page
|
||||
title="Variables"
|
||||
icon={<Variable className="w-8 h-8" />}
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
placeholder="search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-[200px] lg:w-[300px]"
|
||||
/>
|
||||
<CreateVariable />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DataTable
|
||||
tableKey="variables"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Value" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<TextUpdateMenu
|
||||
title={`${row.original.name} - Value`}
|
||||
placeholder="Set value"
|
||||
value={row.original.value}
|
||||
onUpdate={(value) => {
|
||||
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 (
|
||||
<TextUpdateMenu
|
||||
title={`${row.original.name} - Description`}
|
||||
placeholder="Set description"
|
||||
value={row.original.description}
|
||||
onUpdate={(description) => {
|
||||
if (row.original.description === description) {
|
||||
return;
|
||||
}
|
||||
updateDescription({
|
||||
name: row.original.name,
|
||||
description,
|
||||
});
|
||||
}}
|
||||
triggerClassName="w-full"
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
cell: ({ row }) => <DeleteVariable name={row.original.name} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="items-center gap-2">
|
||||
New Variable <PlusCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Variable</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-8 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
Name
|
||||
<Input
|
||||
className="w-72"
|
||||
value={name}
|
||||
onChange={(e) =>
|
||||
setName(e.target.value.toUpperCase().replaceAll(" ", "_"))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex justify-end">
|
||||
<Button className="gap-4" onClick={submit} disabled={isPending}>
|
||||
Submit
|
||||
{isPending ? (
|
||||
<Loader2 className="w-4 animate-spin" />
|
||||
) : (
|
||||
<Check className="w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<ConfirmButton
|
||||
title="Delete"
|
||||
icon={<Trash className="w-4 h-4" />}
|
||||
onClick={() => mutate({ name })}
|
||||
loading={isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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: <Keys /> },
|
||||
{ path: "tags", element: <Tags /> },
|
||||
{ path: "tree", element: <Tree /> },
|
||||
{ path: "resources", element: <AllResources /> },
|
||||
{ path: "alerts", element: <Alerts /> },
|
||||
{ path: "updates", element: <Updates /> },
|
||||
{ path: "variables", element: <Variables /> },
|
||||
{ path: "resources", element: <AllResources /> },
|
||||
{ path: "user-groups/:id", element: <UserGroupPage /> },
|
||||
{
|
||||
path: "users",
|
||||
children: [
|
||||
@@ -36,7 +39,6 @@ const ROUTER = createBrowserRouter([
|
||||
{ path: ":id", element: <UserPage /> },
|
||||
],
|
||||
},
|
||||
{ path: "user-groups/:id", element: <UserGroupPage /> },
|
||||
{
|
||||
path: ":type",
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user