site updates

This commit is contained in:
Henry Dollman
2024-07-10 19:26:18 -04:00
parent 96e4c3b9ea
commit fa9e257864
16 changed files with 670 additions and 57 deletions

View File

@@ -13,11 +13,10 @@ import {
CommandShortcut,
} from '@/components/ui/command'
import { useEffect, useState } from 'react'
import { navigate } from 'wouter/use-browser-location'
import { useStore } from '@nanostores/react'
import { $servers } from '@/lib/stores'
import { $servers, navigate } from '@/lib/stores'
export function CommandPalette() {
export default function () {
const [open, setOpen] = useState(false)
const servers = useStore($servers)

View File

@@ -0,0 +1,117 @@
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
import { formatDateShort } from '@/lib/utils'
const chartData = [
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
{ month: '2024-07-09 23:29:08.976Z', cpu: 6.2 },
{ month: '2024-07-09 23:28:08.976Z', cpu: 2.8 },
{ month: '2024-07-09 23:27:08.976Z', cpu: 9.5 },
{ month: '2024-07-09 23:26:08.976Z', cpu: 23.4 },
{ month: '2024-07-09 23:25:08.976Z', cpu: 4.3 },
{ month: '2024-07-09 23:24:08.976Z', cpu: 9.1 },
]
// for (const data of chartData) {
// data.month = formatDateShort(data.month)
// }
const chartConfig = {
cpu: {
label: 'cpu',
color: 'hsl(var(--chart-1))',
},
} satisfies ChartConfig
export default function () {
return (
<ChartContainer config={chartConfig} className="h-full w-full absolute aspect-auto">
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 0,
top: 7,
bottom: 7,
}}
>
<CartesianGrid vertical={false} />
<YAxis domain={[0, 100]} tickCount={5} tickLine={false} axisLine={false} tickMargin={8} />
<XAxis
dataKey="month"
tickLine={true}
axisLine={false}
tickMargin={8}
minTickGap={30}
tickFormatter={(value) => formatDateShort(value)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
<Area
dataKey="cpu"
type="natural"
fill="var(--color-cpu)"
fillOpacity={0.4}
stroke="var(--color-cpu)"
/>
</AreaChart>
</ChartContainer>
)
}

View File

@@ -0,0 +1,53 @@
'use client'
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from '@/components/ui/chart'
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: 'Desktop',
color: '#2563eb',
},
mobile: {
label: 'Mobile',
color: '#60a5fa',
},
} satisfies ChartConfig
export function Component() {
return (
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
</BarChart>
</ChartContainer>
)
}

View File

View File

@@ -1,11 +1,9 @@
import { Link } from 'wouter'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
import { UserAuthForm } from '@/components/user-auth-form'
import { ChevronLeft } from 'lucide-react'
export default function LoginPage() {
export default function () {
return (
<div className="container relative hidden h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0">
<div className="lg:p-8">
@@ -23,9 +21,9 @@ export default function LoginPage() {
</div>
<UserAuthForm />
<p className="px-8 text-center text-sm text-muted-foreground">
<Link href="/register" className="hover:text-brand underline underline-offset-4">
<a href="/register" className="hover:text-brand underline underline-offset-4">
Don&apos;t have an account? Sign Up
</Link>
</a>
</p>
</div>
</div>

View File

@@ -1,11 +1,13 @@
import { useEffect } from 'react'
import { Suspense, lazy, useEffect } from 'react'
import { $servers, pb } from '@/lib/stores'
import { DataTable } from '../server-table/data-table'
// import { DataTable } from '../server-table/data-table'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
import { SystemRecord } from '@/types'
import { updateServerList } from '@/lib/utils'
export function Home() {
const DataTable = lazy(() => import('../server-table/data-table'))
export default function () {
useEffect(() => {
document.title = 'Home'
}, [])
@@ -57,7 +59,9 @@ export function Home() {
</CardDescription>
</CardHeader>
<CardContent>
<DataTable />
<Suspense>
<DataTable />
</Suspense>
</CardContent>
</Card>
</>

View File

@@ -1,24 +1,32 @@
import { $servers, pb } from '@/lib/stores'
import { ContainerStatsRecord, SystemRecord } from '@/types'
import { useEffect, useState } from 'react'
import { useRoute } from 'wouter'
import { Suspense, lazy, useEffect, useState } from 'react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card'
import { useStore } from '@nanostores/react'
import Spinner from '../spinner'
// import { CpuChart } from '../cpu-chart'
const CpuChart = lazy(() => import('../cpu-chart'))
function timestampToBrowserTime(timestamp: string) {
const date = new Date(timestamp)
return date.toLocaleString()
}
export function ServerDetail() {
// function addColors(objects: Record<string, any>[]) {
// objects.forEach((obj, index) => {
// const hue = ((index * 360) / objects.length) % 360 // Distribute hues evenly
// obj.fill = `hsl(${hue}, 100%, 50%)` // Set fill to HSL color with full saturation and 50% lightness
// })
// }
export default function ServerDetail({ name }: { name: string }) {
const servers = useStore($servers)
const [_, params] = useRoute('/server/:name')
const [server, setServer] = useState({} as SystemRecord)
const [containers, setContainers] = useState([] as ContainerStatsRecord[])
// const [serverId, setServerId] = useState('')
useEffect(() => {
document.title = params!.name
document.title = name
}, [])
useEffect(() => {
@@ -27,7 +35,7 @@ export function ServerDetail() {
return
}
console.log('running')
const matchingServer = servers.find((s) => s.name === params!.name) as SystemRecord
const matchingServer = servers.find((s) => s.name === name) as SystemRecord
setServer(matchingServer)
@@ -52,6 +60,20 @@ export function ServerDetail() {
return (
<>
<div className="grid grid-cols-1 gap-10">
<Card>
<CardHeader>
<CardTitle>CPU Usage</CardTitle>
<CardDescription>Showing total visitors for the last 30 minutes</CardDescription>
</CardHeader>
<CardContent className="pl-0 w-[calc(100%-2em)] h-72 relative">
<Suspense fallback={<Spinner />}>
<CpuChart />
</Suspense>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className={'mb-3'}>{server.name}</CardTitle>

View File

@@ -48,18 +48,15 @@ import {
MoreHorizontal,
ArrowUpDown,
Copy,
RefreshCcw,
Server,
Cpu,
MemoryStick,
HardDrive,
} from 'lucide-react'
import { useMemo, useState } from 'react'
import { navigate } from 'wouter/use-browser-location'
import { $servers, pb } from '@/lib/stores'
import { $servers, pb, navigate } from '@/lib/stores'
import { useStore } from '@nanostores/react'
import { AddServerButton } from '../add-server'
import clsx from 'clsx'
import { cn, copyToClipboard } from '@/lib/utils'
function CellFormatter(info: CellContext<SystemRecord, unknown>) {
@@ -74,7 +71,7 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
<div className="flex gap-2 items-center">
<span className="grow block bg-muted h-4 relative rounded-sm overflow-hidden">
<span
className={clsx('absolute inset-0 w-full h-full origin-left', `bg-${color}-500`)}
className={cn('absolute inset-0 w-full h-full origin-left', `bg-${color}-500`)}
style={{ transform: `scalex(${val}%)` }}
></span>
</span>
@@ -97,7 +94,7 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: string, Ico
)
}
export function DataTable() {
export default function () {
const data = useStore($servers)
const [deleteServer, setDeleteServer] = useState({} as SystemRecord)
const [sorting, setSorting] = useState<SortingState>([])
@@ -111,7 +108,7 @@ export function DataTable() {
cell: (info) => (
<span className="flex gap-1 items-center text-base">
<span
className={clsx(
className={cn(
'w-2.5 h-2.5 block left-0 rounded-full',
info.row.original.active ? 'bg-green-500' : 'bg-red-500'
)}

View File

@@ -0,0 +1,9 @@
import { LoaderCircleIcon } from 'lucide-react'
export default function () {
return (
<div className="grid place-content-center h-full absolute inset-0">
<LoaderCircleIcon className="animate-spin h-10 w-10 opacity-60" />
</div>
)
}

View File

@@ -0,0 +1,361 @@
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES).map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && (item?.value !== undefined) && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}