choose between config / log for deployment

This commit is contained in:
mbecker20
2024-05-11 16:06:36 -07:00
parent 1cc1813185
commit 70ff93050f
10 changed files with 305 additions and 114 deletions
+2
View File
@@ -24,6 +24,8 @@
"@radix-ui/react-switch": "1.0.3",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-toast": "1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@tanstack/react-query": "5.35.1",
"@tanstack/react-table": "8.16.0",
"ansi-to-html": "0.7.2",
+36 -27
View File
@@ -30,6 +30,7 @@ export const ConfigLayout = <
onConfirm,
onReset,
selector,
titleOther,
}: {
config: Partial<T>;
children: ReactNode;
@@ -37,33 +38,38 @@ export const ConfigLayout = <
onConfirm: () => void;
onReset: () => void;
selector?: ReactNode;
}) => (
<Section
title="Config"
icon={<Settings className="w-4 h-4" />}
actions={
<div className="flex gap-2">
{selector}
<Button
variant="outline"
onClick={onReset}
disabled={disabled || (config ? !Object.keys(config).length : true)}
>
<History className="w-4 h-4" />
</Button>
{Object.keys(config).length ? (
<ConfirmUpdate
content={JSON.stringify(config, null, 2)}
onConfirm={onConfirm}
disabled={disabled}
/>
) : null}
</div>
}
>
{children}
</Section>
);
titleOther?: ReactNode;
}) => {
const titleProps = titleOther
? { titleOther }
: { title: "Config", icon: <Settings className="w-4 h-4" /> };
return (
<Section
{...titleProps}
actions={
<div className="flex gap-2">
{selector}
<Button
variant="outline"
onClick={onReset}
disabled={disabled || (config ? !Object.keys(config).length : true)}
>
<History className="w-4 h-4" />
</Button>
{Object.keys(config).length ? (
<ConfirmUpdate
content={JSON.stringify(config, null, 2)}
onConfirm={onConfirm}
disabled={disabled}
/>
) : null}
</div>
}
>
{children}
</Section>
);
};
export const Config = <T,>({
config,
@@ -73,6 +79,7 @@ export const Config = <T,>({
onSave,
components,
selector,
titleOther,
}: {
config: T;
update: Partial<T>;
@@ -80,6 +87,7 @@ export const Config = <T,>({
set: React.Dispatch<SetStateAction<Partial<T>>>;
onSave: () => Promise<void>;
selector?: ReactNode;
titleOther?: ReactNode;
components: Record<
string,
Record<
@@ -96,6 +104,7 @@ export const Config = <T,>({
return (
<ConfigLayout
titleOther={titleOther}
config={update}
disabled={disabled}
onConfirm={async () => {
+9 -5
View File
@@ -37,6 +37,7 @@ export const Layout = () => {
interface PageProps {
title?: ReactNode;
titleRight?: ReactNode;
titleOther?: ReactNode;
children?: ReactNode;
subtitle?: ReactNode;
actions?: ReactNode;
@@ -45,6 +46,7 @@ interface PageProps {
export const Page = ({
title,
titleRight,
titleOther,
subtitle,
actions,
children,
@@ -62,26 +64,28 @@ export const Page = ({
{actions}
</div>
)}
{titleOther}
{children}
</div>
);
interface SectionProps {
title?: ReactNode;
children?: ReactNode;
icon?: ReactNode;
titleOther?: ReactNode;
children?: ReactNode;
actions?: ReactNode;
}
export const Section = ({ title, icon, actions, children }: SectionProps) => (
export const Section = ({ title, icon, titleOther, actions, children }: SectionProps) => (
<div className="flex flex-col gap-4">
<div className="flex items-start justify-between">
{(title || icon) && (
{(title || icon) ? (
<div className="flex items-center gap-2 text-muted-foreground">
{icon}
<h2 className="text-xl">{title}</h2>
{title && <h2 className="text-xl">{title}</h2>}
</div>
)}
) : titleOther}
{actions}
</div>
{children}
@@ -1,6 +1,6 @@
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { useState } from "react";
import { ReactNode, useState } from "react";
import { AccountSelector, ConfigItem } from "@components/config/util";
import { ImageConfig } from "./components/image";
import { RestartModeSelector } from "./components/restart";
@@ -18,7 +18,7 @@ import {
import { LabelsConfig, ServerSelector } from "@components/resources/common";
import { TextUpdateMenu } from "@components/util";
export const DeploymentConfig = ({ id }: { id: string }) => {
export const DeploymentConfig = ({ id, titleOther }: { id: string; titleOther: ReactNode }) => {
const perms = useRead("GetPermissionLevel", {
target: { type: "Deployment", id },
}).data;
@@ -38,6 +38,7 @@ export const DeploymentConfig = ({ id }: { id: string }) => {
return (
<Config
titleOther={titleOther}
disabled={disabled}
config={config}
update={update}
@@ -4,7 +4,6 @@ import { RequiredResourceComponents } from "@types";
import { HardDrive, Rocket } from "lucide-react";
import { cn } from "@lib/utils";
import { useServer } from "../server";
import { DeploymentConfig } from "./config";
import {
DeployContainer,
StartOrStopContainer,
@@ -24,12 +23,73 @@ import { DeploymentsChart } from "./dashboard";
import { NewResource, ResourceLink } from "../common";
import { Card, CardHeader } from "@ui/card";
import { RunBuild } from "../build/actions";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { DeploymentConfig } from "./config";
import { atomWithStorage } from "jotai/utils";
import { useAtom } from "jotai";
const configOrLog = atomWithStorage("config-or-log-v1", "Config");
export const useDeployment = (id?: string) =>
useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.find(
(d) => d.id === id
);
const ConfigOrLog = ({ id }: { id: string }) => {
const [view, setView] = useAtom(configOrLog);
const state = useDeployment(id)?.info.state;
const logsDisabled =
state === undefined ||
state === Types.DockerContainerState.Unknown ||
state === Types.DockerContainerState.NotDeployed;
return (
<Tabs
value={logsDisabled ? "Config" : view}
onValueChange={setView}
className="grid gap-4"
>
<TabsContent value="Config">
<DeploymentConfig
id={id}
titleOther={
<TabsList className="justify-start w-fit">
<TabsTrigger value="Config" className="w-[70px]">
Config
</TabsTrigger>
<TabsTrigger
value="Log"
className="w-[70px]"
disabled={logsDisabled}
>
Log
</TabsTrigger>
</TabsList>
}
/>
</TabsContent>
<TabsContent value="Log">
<DeploymentLogs
id={id}
titleOther={
<TabsList className="justify-start w-fit">
<TabsTrigger value="Config" className="w-[70px]">
Config
</TabsTrigger>
<TabsTrigger
value="Log"
className="w-[70px]"
disabled={logsDisabled}
>
Log
</TabsTrigger>
</TabsList>
}
/>
</TabsContent>
</Tabs>
);
};
export const DeploymentComponents: RequiredResourceComponents = {
Dashboard: DeploymentsChart,
@@ -106,9 +166,9 @@ export const DeploymentComponents: RequiredResourceComponents = {
RemoveContainer,
},
Page: { DeploymentLogs },
Page: {},
Config: DeploymentConfig,
Config: ConfigOrLog,
DangerZone: ({ id }) => (
<>
@@ -1,16 +1,14 @@
import { Section } from "@components/layouts";
import { useRead } from "@lib/hooks";
import { Types } from "@monitor/client";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@ui/tabs";
import { Button } from "@ui/button";
import {
TerminalSquare,
AlertOctagon,
RefreshCw,
ChevronDown,
X,
AlertOctagon,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { useDeployment } from ".";
import {
Select,
@@ -23,8 +21,9 @@ import {
import { Input } from "@ui/input";
import { useToast } from "@ui/use-toast";
import { logToHtml } from "@lib/utils";
import { ToggleGroup, ToggleGroupItem } from "@ui/toggle-group";
export const DeploymentLogs = ({ id }: { id: string }) => {
export const DeploymentLogs = ({ id, titleOther }: { id: string; titleOther: ReactNode }) => {
const state = useDeployment(id)?.info.state;
if (
state === undefined ||
@@ -33,11 +32,12 @@ export const DeploymentLogs = ({ id }: { id: string }) => {
) {
return null;
}
return <DeploymentLogsInner id={id} />;
return <DeploymentLogsInner id={id} titleOther={titleOther} />;
};
const DeploymentLogsInner = ({ id }: { id: string }) => {
const DeploymentLogsInner = ({ id, titleOther }: { id: string; titleOther: ReactNode }) => {
const { toast } = useToast();
const [stream, setStream] = useState("stdout");
const [tail, set] = useState("100");
const [terms, setTerms] = useState<string[]>([]);
const [search, setSearch] = useState("");
@@ -60,88 +60,77 @@ const DeploymentLogsInner = ({ id }: { id: string }) => {
const { Log, refetch, stderr } = terms.length
? SearchLogs(id, terms)
: NoSearchLogs(id, tail);
: NoSearchLogs(id, tail, stream);
return (
<Tabs defaultValue="stdout">
<Section
title="Logs"
icon={<TerminalSquare className="w-4 h-4" />}
actions={
<div className="flex gap-2">
{terms.map((term, index) => (
<Button
key={term}
variant="destructive"
onClick={() => setTerms(terms.filter((_, i) => i !== index))}
className="flex gap-2 items-center py-0 px-2"
>
{term}
<X className="w-4 h-h" />
</Button>
))}
<div className="relative">
<Input
placeholder="Search Logs"
value={search}
onChange={(e) => setSearch(e.target.value)}
onBlur={addTerm}
onKeyDown={(e) => {
if (e.key === "Enter") addTerm();
}}
className="w-[300px]"
/>
<Button
variant="ghost"
size="icon"
onClick={clearSearch}
className="absolute right-0 top-1/2 -translate-y-1/2"
>
<X className="w-4 h-4" />
</Button>
</div>
<TabsList>
<TabsTrigger value="stdout">stdout</TabsTrigger>
<TabsTrigger value="stderr">
stderr
{stderr && (
<AlertOctagon className="w-4 h-4 ml-2 stroke-red-500" />
)}
</TabsTrigger>
</TabsList>
<Button variant="secondary" size="icon" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4" />
<Section
titleOther={titleOther}
actions={
<div className="flex gap-2">
{terms.map((term, index) => (
<Button
key={term}
variant="destructive"
onClick={() => setTerms(terms.filter((_, i) => i !== index))}
className="flex gap-2 items-center py-0 px-2"
>
{term}
<X className="w-4 h-h" />
</Button>
<TailLengthSelector
selected={tail}
onSelect={set}
disabled={search.length > 0}
))}
<div className="relative">
<Input
placeholder="Search Logs"
value={search}
onChange={(e) => setSearch(e.target.value)}
onBlur={addTerm}
onKeyDown={(e) => {
if (e.key === "Enter") addTerm();
}}
className="w-[300px]"
/>
<Button
variant="ghost"
size="icon"
onClick={clearSearch}
className="absolute right-0 top-1/2 -translate-y-1/2"
>
<X className="w-4 h-4" />
</Button>
</div>
}
>
{Log}
</Section>
</Tabs>
<ToggleGroup type="single" value={stream} onValueChange={setStream}>
<ToggleGroupItem value="stdout">stdout</ToggleGroupItem>
<ToggleGroupItem value="stderr">
stderr
{stderr && (
<AlertOctagon className="w-4 h-4 ml-2 stroke-red-500" />
)}
</ToggleGroupItem>
</ToggleGroup>
<Button variant="secondary" size="icon" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4" />
</Button>
<TailLengthSelector
selected={tail}
onSelect={set}
disabled={search.length > 0}
/>
</div>
}
>
{Log}
</Section>
);
};
const NoSearchLogs = (id: string, tail: string) => {
const NoSearchLogs = (id: string, tail: string, stream: string) => {
const { data: log, refetch } = useRead(
"GetLog",
{ deployment: id, tail: Number(tail) },
{ refetchInterval: 30000 }
);
return {
Log: (
<>
{["stdout", "stderr"].map((stream) => (
<TabsContent key={stream} className="h-full relative" value={stream}>
<Log log={log} stream={stream as "stdout" | "stderr"} />
</TabsContent>
))}
</>
),
Log: <Log log={log} stream={stream as "stdout" | "stderr"} />,
refetch,
stderr: !!log?.stderr,
};
+2 -2
View File
@@ -48,8 +48,8 @@
--secondary: 220.1 40.1% 90.2%;
--secondary-foreground: 220.1 40.1% 5.2%;
--muted: 220.1 40.1% 98.2%;
--muted-foreground: 220.1 20.1% 40.2%;
--muted: 220.1 40.1% 90.2%;
--muted-foreground: 220.1 20.1% 20.2%;
--accent: 220.1 40.1% 90.2%;
--accent-foreground: 220.1 40.1% 11.2%;
+59
View File
@@ -0,0 +1,59 @@
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { VariantProps } from "class-variance-authority"
import { cn } from "@lib/utils"
import { toggleVariants } from "@//ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }
+43
View File
@@ -0,0 +1,43 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-3",
sm: "h-8 px-2",
lg: "h-10 px-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }
+24
View File
@@ -889,6 +889,30 @@
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-visually-hidden" "1.0.3"
"@radix-ui/react-toggle-group@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz#f5b5c8c477831b013bec3580c55e20a68179d6ec"
integrity sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-direction" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-toggle" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-toggle@1.0.3", "@radix-ui/react-toggle@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz#aecb2945630d1dc5c512997556c57aba894e539e"
integrity sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-callback-ref@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"