Skip to content

Commit ef31d04

Browse files
committed
cleanup dashboard and add region filter
1 parent 330c6f7 commit ef31d04

32 files changed

+1477
-170
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
singleQuote: true,
3+
semi: true,
4+
trailingComma: 'all',
5+
tabWidth: 2,
6+
printWidth: 100,
7+
endOfLine: 'lf',
8+
plugins: ['prettier-plugin-tailwindcss'],
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Table } from '@tanstack/react-table';
2+
import { TableFilter } from './TableFilter';
3+
import { Message } from '@/page';
4+
5+
interface FiltersProps {
6+
table: Table<Message>;
7+
}
8+
9+
export default function Filters({ table }: Readonly<FiltersProps>) {
10+
return (
11+
<div className="flex flex-row gap-2">
12+
<TableFilter
13+
table={table}
14+
column={table.getColumn('region')}
15+
title="Region"
16+
options={
17+
table.getColumn('region')?.getFacetedUniqueValues()
18+
? Array.from(
19+
(table.getColumn('region')?.getFacetedUniqueValues() ?? new Map()).keys(),
20+
).map((value) => {
21+
return { label: value, value };
22+
})
23+
: []
24+
}
25+
/>
26+
</div>
27+
);
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
interface FooterProps {
2+
isConnected: boolean;
3+
}
4+
5+
export default function Footer({ isConnected }: FooterProps) {
6+
return (
7+
<div className="mt-auto flex flex-col justify-between bg-white p-4 text-sm text-gray-600 shadow-md md:flex-row">
8+
<div>
9+
<span className="font-medium">CosmosDB Status:</span>{' '}
10+
{isConnected ? 'Connected' : 'Not Connected'}
11+
</div>
12+
<div suppressHydrationWarning>
13+
Event Puller Dashboard | {new Date().getFullYear()} | Expanso.io
14+
</div>
15+
</div>
16+
);
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Message } from '../page';
2+
3+
interface StatsProps {
4+
vmStates: Map<string, Message>;
5+
queueSize: number;
6+
messageCount: number;
7+
lastPoll: string;
8+
}
9+
10+
export default function Stats({ vmStates, queueSize, messageCount, lastPoll }: StatsProps) {
11+
return (
12+
<div className="grid grid-cols-1 gap-4 p-4 md:grid-cols-4">
13+
<div className="rounded bg-white p-4 shadow-md">
14+
<div className="mb-1 text-sm text-gray-500">Unique VMs</div>
15+
<div className="text-2xl font-bold">{vmStates.size}</div>
16+
</div>
17+
<div className="rounded bg-white p-4 shadow-md">
18+
<div className="mb-1 text-sm text-gray-500">Queue Size</div>
19+
<div className="text-2xl font-bold">{queueSize}</div>
20+
</div>
21+
<div className="rounded bg-white p-4 shadow-md">
22+
<div className="mb-1 text-sm text-gray-500">Messages Processed</div>
23+
<div className="text-2xl font-bold">{messageCount}</div>
24+
</div>
25+
<div className="rounded bg-white p-4 shadow-md">
26+
<div className="mb-1 text-sm text-gray-500">Last Update</div>
27+
<div className="text-2xl font-bold">{lastPoll}</div>
28+
</div>
29+
</div>
30+
);
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import * as React from 'react';
2+
import { Column, Table } from '@tanstack/react-table';
3+
import { Check, PlusCircle } from 'lucide-react';
4+
5+
import { cn } from '@/lib/utils';
6+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
7+
import { Button } from '@/components/ui/button';
8+
import { Separator } from '@/components/ui/separator';
9+
import { Badge } from '@/components/ui/badge';
10+
import {
11+
Command,
12+
CommandEmpty,
13+
CommandGroup,
14+
CommandInput,
15+
CommandItem,
16+
CommandList,
17+
CommandSeparator,
18+
} from './ui/command';
19+
20+
interface TableFilterProps<TData, TValue> {
21+
table: Table<TData>;
22+
column?: Column<TData, TValue>;
23+
title?: string;
24+
options: {
25+
label: string;
26+
value: string;
27+
icon?: React.ComponentType<{ className?: string }>;
28+
}[];
29+
disabled?: boolean;
30+
}
31+
32+
export function TableFilter<TData, TValue>({
33+
table,
34+
column,
35+
title,
36+
options,
37+
disabled = false,
38+
}: Readonly<TableFilterProps<TData, TValue>>) {
39+
const facets = column?.getFacetedUniqueValues();
40+
const selectedValues = new Set(column?.getFilterValue() as string[]);
41+
42+
const isDropdownVariant = true;
43+
44+
if (isDropdownVariant) {
45+
return (
46+
<Popover>
47+
<PopoverTrigger asChild>
48+
<Button disabled={disabled} variant="outline" size="sm" className="h-8 border-dashed">
49+
<PlusCircle />
50+
{title}
51+
{selectedValues?.size > 0 && (
52+
<>
53+
<Separator orientation="vertical" className="mx-2 h-4" />
54+
<Badge variant="secondary" className="rounded-sm px-1 font-normal lg:hidden">
55+
{selectedValues.size}
56+
</Badge>
57+
<div className="hidden space-x-1 lg:flex">
58+
{selectedValues.size > 2 ? (
59+
<Badge variant="secondary" className="rounded-sm px-1 font-normal">
60+
{selectedValues.size} Selected
61+
</Badge>
62+
) : (
63+
options
64+
.filter((option) => selectedValues.has(option.value))
65+
.map((option) => (
66+
<Badge
67+
variant="secondary"
68+
key={option.value}
69+
className="rounded-sm px-1 font-normal"
70+
>
71+
{option.label}
72+
</Badge>
73+
))
74+
)}
75+
</div>
76+
</>
77+
)}
78+
</Button>
79+
</PopoverTrigger>
80+
<PopoverContent className="p-0">
81+
<Command>
82+
<CommandInput placeholder={title} />
83+
<CommandList>
84+
<CommandEmpty>
85+
<span className="italic">No results found</span>
86+
</CommandEmpty>
87+
<CommandGroup>
88+
{options.map((option) => {
89+
const isSelected = selectedValues.has(option.value);
90+
return (
91+
<CommandItem
92+
key={option.value}
93+
onSelect={() => {
94+
if (isSelected) {
95+
selectedValues.delete(option.value);
96+
} else {
97+
selectedValues.add(option.value);
98+
}
99+
const filterValues = Array.from(selectedValues);
100+
column?.setFilterValue(filterValues.length ? filterValues : undefined);
101+
table.resetRowSelection();
102+
}}
103+
>
104+
<div
105+
className={cn(
106+
'border-primary mr-2 flex h-4 w-4 items-center justify-center rounded-sm border',
107+
isSelected
108+
? 'bg-primary text-primary-foreground'
109+
: 'opacity-50 [&_svg]:invisible',
110+
)}
111+
>
112+
<Check />
113+
</div>
114+
{option.icon && (
115+
<option.icon className="text-muted-foreground mr-2 h-4 w-4" />
116+
)}
117+
<span>{option.label}</span>
118+
{facets?.get(option.value) && (
119+
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
120+
{facets.get(option.value)}
121+
</span>
122+
)}
123+
</CommandItem>
124+
);
125+
})}
126+
</CommandGroup>
127+
{selectedValues.size > 0 && (
128+
<>
129+
<CommandSeparator />
130+
<CommandGroup>
131+
<CommandItem
132+
onSelect={() => column?.setFilterValue(undefined)}
133+
className="justify-center text-center"
134+
>
135+
Clear filters
136+
</CommandItem>
137+
</CommandGroup>
138+
</>
139+
)}
140+
</CommandList>
141+
</Command>
142+
</PopoverContent>
143+
</Popover>
144+
);
145+
}
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import { DropdownMenuCheckboxItemProps } from '@radix-ui/react-dropdown-menu';
5+
6+
import { Button } from '@/components/ui/button';
7+
import {
8+
DropdownMenu,
9+
DropdownMenuCheckboxItem,
10+
DropdownMenuContent,
11+
DropdownMenuLabel,
12+
DropdownMenuSeparator,
13+
DropdownMenuTrigger,
14+
} from '@/components/ui/dropdown-menu';
15+
16+
type Checked = DropdownMenuCheckboxItemProps['checked'];
17+
18+
export function DropdownMenuCheckboxes() {
19+
const [showStatusBar, setShowStatusBar] = React.useState<Checked>(true);
20+
const [showActivityBar, setShowActivityBar] = React.useState<Checked>(false);
21+
const [showPanel, setShowPanel] = React.useState<Checked>(false);
22+
23+
return (
24+
<DropdownMenu>
25+
<DropdownMenuTrigger asChild>
26+
<Button variant="outline">Open</Button>
27+
</DropdownMenuTrigger>
28+
<DropdownMenuContent className="w-56">
29+
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
30+
<DropdownMenuSeparator />
31+
<DropdownMenuCheckboxItem checked={showStatusBar} onCheckedChange={setShowStatusBar}>
32+
Status Bar
33+
</DropdownMenuCheckboxItem>
34+
<DropdownMenuCheckboxItem
35+
checked={showActivityBar}
36+
onCheckedChange={setShowActivityBar}
37+
disabled
38+
>
39+
Activity Bar
40+
</DropdownMenuCheckboxItem>
41+
<DropdownMenuCheckboxItem checked={showPanel} onCheckedChange={setShowPanel}>
42+
Panel
43+
</DropdownMenuCheckboxItem>
44+
</DropdownMenuContent>
45+
</DropdownMenu>
46+
);
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { truncateText } from '@/lib/utils';
2+
import { Message } from '@/page';
3+
import { useEffect, useState } from 'react';
4+
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
5+
6+
interface VMCardProps {
7+
message: Message;
8+
}
9+
10+
function getContrastTextColor(hexColor: string): string {
11+
// Remove the hash if it's there
12+
const color = hexColor.charAt(0) === '#' ? hexColor.substring(1) : hexColor;
13+
const r = parseInt(color.substring(0, 2), 16);
14+
const g = parseInt(color.substring(2, 4), 16);
15+
const b = parseInt(color.substring(4, 6), 16);
16+
17+
// Calculate brightness
18+
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
19+
return brightness > 128 ? 'black' : 'white';
20+
}
21+
22+
export default function VMCard({ message }: Readonly<VMCardProps>) {
23+
const [showIcon, setShowIcon] = useState(false);
24+
25+
useEffect(() => {
26+
setShowIcon(true);
27+
const timeout = setTimeout(() => {
28+
setShowIcon(false);
29+
}, 500);
30+
return () => clearTimeout(timeout);
31+
}, [message]);
32+
33+
return (
34+
<Popover>
35+
<PopoverTrigger asChild>
36+
<div
37+
key={message.vm_name}
38+
className="relative flex h-24 cursor-pointer flex-col items-center justify-center rounded-sm p-2 hover:-translate-y-1"
39+
style={{ backgroundColor: message.color, color: getContrastTextColor(message.color) }}
40+
>
41+
{/* Icon (flash when updated) */}
42+
<div
43+
className={`pointer-events-none absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${showIcon ? 'opacity-100' : 'opacity-0'}`}
44+
>
45+
<div className="text-2xl">{message.icon_name}</div>
46+
</div>
47+
48+
{/* Content */}
49+
<div
50+
className={`transition-opacity duration-500 ${showIcon ? 'opacity-0' : 'opacity-100'}`}
51+
>
52+
<div
53+
className="w-full truncate text-center text-xs font-medium"
54+
title={message.vm_name}
55+
>
56+
{truncateText(message.vm_name, 8)}
57+
</div>
58+
<div className="w-full truncate text-xs" title={message.container_id}>
59+
{truncateText(message.container_id, 8)}
60+
</div>
61+
</div>
62+
</div>
63+
</PopoverTrigger>
64+
<PopoverContent>
65+
<div className="text-sm">
66+
<p>{message.vm_name}</p>
67+
<p>{message.container_id}</p>
68+
</div>
69+
</PopoverContent>
70+
</Popover>
71+
);
72+
}

0 commit comments

Comments
 (0)