Skip to content

Commit

Permalink
feat(website): split report into tabs, refactor report module
Browse files Browse the repository at this point in the history
  • Loading branch information
vst committed Mar 30, 2024
1 parent 109d778 commit 9719020
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 288 deletions.
4 changes: 2 additions & 2 deletions website/src/app/report/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AppMain } from '@/components/app';
import { Report } from '@/components/report';

export default function Page() {
return <AppMain />;
return <Report />;
}
File renamed without changes.
93 changes: 93 additions & 0 deletions website/src/components/report/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { LhpHostReport, LhpPatrolReport } from '@/lib/data';
import { Tab, Tabs } from '@nextui-org/react';
import { Just, Maybe, Nothing } from 'purify-ts/Maybe';
import { useEffect, useState } from 'react';
import { ShowHostDetails } from './ShowHostDetails';
import { Sidebar } from './Sidebar';
import { TabulateHosts } from './TabulateHosts';

export function App({ data, onFlushRequest }: { data: LhpPatrolReport; onFlushRequest: () => void }) {
const [host, setHost] = useState<Maybe<LhpHostReport>>(Nothing);
type TabKey = 'overview' | 'tabulate-hosts' | 'show-host-details' | 'flush';
const [tab, setTab] = useState<TabKey>('overview');

useEffect(() => {
if (host.isJust()) {
setTab('show-host-details');
}
}, [host]);

return (
<div className="flex w-full flex-col">
<Tabs
aria-label="Report Views"
fullWidth
radius="none"
color="secondary"
size="lg"
className="border-b"
selectedKey={tab}
onSelectionChange={(x) => {
if (x === 'flush') {
onFlushRequest();
} else {
setTab(x as TabKey);
}
}}
>
<Tab key="overview" title="🏖️ Overview" className="py-0">
<TabOverview />
</Tab>

<Tab key="tabulate-hosts" title="🗒️ Tabulate Hosts" className="py-0">
<TabTabulateHosts data={data} setHost={setHost} />
</Tab>

<Tab key="show-host-details" title="🔬 Host Details" className="py-0">
<TabShowHostDetails data={data} host={host} setHost={setHost} />
</Tab>

<Tab key="flush" title="❌ Flush Data" className="py-0"></Tab>
</Tabs>
</div>
);
}

export function TabOverview() {
return <div>Overview is coming soon...</div>;
}

export function TabTabulateHosts({
data,
setHost,
}: {
data: LhpPatrolReport;
setHost: (x: Maybe<LhpHostReport>) => void;
}) {
return <TabulateHosts hosts={data.hosts} onHostSelect={(x) => setHost(Just(x))} />;
}

export function TabShowHostDetails({
data,
host,
setHost,
}: {
data: LhpPatrolReport;
host: Maybe<LhpHostReport>;
setHost: (x: Maybe<LhpHostReport>) => void;
}) {
return (
<div className="grid grid-cols-6">
<div className="border-r bg-gray-100 shadow-lg">
<Sidebar data={data.hosts} onHostSelect={(x) => setHost(Just(x))} />
</div>

<div className="col-span-5">
{host.caseOf({
Nothing: () => <div className="p-4 text-red-400">Choose a host to view details.</div>,
Just: (x) => <ShowHostDetails host={x} />,
})}
</div>
</div>
);
}
50 changes: 50 additions & 0 deletions website/src/components/report/DataLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { LhpPatrolReport, parseData, saveData } from '@/lib/data';
import { Card, CardBody, CardFooter, CardHeader } from '@nextui-org/card';
import { Divider } from '@nextui-org/divider';
import { ChangeEvent, useState } from 'react';
import { Centered } from '../helpers';

export function DataLoader({ onLoadData }: { onLoadData: (x: LhpPatrolReport) => void }) {
const [error, setError] = useState<string>();

const changeHandler = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setError(undefined);

const files = (e.target as HTMLInputElement).files;

if (files == null || files.length === 0) {
return;
}

const fr = new FileReader();
fr.onloadend = () =>
parseData(fr.result as string).caseOf({
Left: setError,
Right(data) {
saveData(data);
onLoadData(data);
},
});
fr.readAsText(files[0]);
};

return (
<Centered>
<Card radius="sm" shadow="sm" fullWidth={true} classNames={{ base: 'max-w-xl' }}>
<CardHeader className="text-lg font-bold">Load Data</CardHeader>

<Divider />

<CardBody>
<input type="file" id="image" accept=".JSON" onChange={changeHandler} />
</CardBody>

{error && (
<CardFooter className="bg-red-500 text-white">
<p>{error}</p>
</CardFooter>
)}
</Card>
</Centered>
);
}
138 changes: 138 additions & 0 deletions website/src/components/report/ShowHostDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { LhpHostReport } from '@/lib/data';
import { Card, CardBody, CardHeader } from '@nextui-org/card';
import { Chip } from '@nextui-org/chip';
import { Listbox, ListboxItem } from '@nextui-org/listbox';
import Link from 'next/link';
import { toast } from 'react-toastify';
import { KVBox } from '../helpers';

export function ShowHostDetails({ host }: { host: LhpHostReport }) {
return (
<div className="space-y-4 px-4 py-4">
<h1 className="flex flex-row justify-between text-xl font-bold">
<div className="space-x-2">
<span>{host.host.name}</span>
{host.host.url && (
<Link href={host.host.url} target="_blank">
🔗
</Link>
)}
</div>

<div className="space-x-1">
{(host.host.tags || []).map((x) => (
<Chip key={x} size="sm" color="primary" variant="flat" radius="sm">
{x}
</Chip>
))}
</div>
</h1>

<KVBox
title="Cloud"
kvs={[
{ key: 'Name', value: host.cloud.name },
{ key: 'ID', value: host.cloud.id },
{ key: 'Type', value: host.cloud.hostType },
{ key: 'Region', value: host.cloud.hostRegion },
{ key: 'Availability Zone', value: host.cloud.hostAvailabilityZone },
{ key: 'Local Hostname', value: host.cloud.hostLocalHostname },
{ key: 'Local Address', value: host.cloud.hostLocalAddress },
{ key: 'Remote Hostname', value: host.cloud.hostRemoteHostname },
{ key: 'Remote Address', value: host.cloud.hostRemoteAddress },
{ key: 'Reserved Address', value: host.cloud.hostReservedAddress },
]}
/>

<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<KVBox
title="Distribution"
kvs={[
{ key: 'ID', value: host.distribution.id },
{ key: 'Name', value: host.distribution.name },
{ key: 'Description', value: host.distribution.description },
{ key: 'Release', value: host.distribution.release },
{ key: 'Version', value: host.distribution.version },
{ key: 'Codename', value: host.distribution.codename },
]}
/>

<KVBox
title="Kernel"
kvs={[
{ key: 'Node', value: host.kernel.node },
{ key: 'Name', value: host.kernel.name },
{ key: 'Machine', value: host.kernel.machine },
{ key: 'Release', value: host.kernel.release },
{ key: 'Version', value: host.kernel.version },
{ key: 'Operating System', value: host.kernel.os },
]}
/>
</div>

<Card radius="sm" shadow="sm">
<CardHeader className="text-lg font-bold">Authorized SSH Keys</CardHeader>

<CardBody>
<Listbox
items={host.authorizedSshKeys}
emptyContent={<span className="text-orange-400">No authorized SSH keys are found. Sounds weird?</span>}
>
{({ length, type, fingerprint, data, comment }) => (
<ListboxItem
key={data}
description={data}
onPress={() => {
navigator.clipboard.writeText(data);
toast('SSH Key is copied to clipboard.');
}}
>
{`${type} (${length}) - ${fingerprint} - ${comment || ''}`}
</ListboxItem>
)}
</Listbox>
</CardBody>
</Card>

<Card radius="sm" shadow="sm">
<CardHeader className="text-lg font-bold">Docker Containers</CardHeader>

<CardBody>
{host.dockerContainers ? (
<Listbox
items={[
...host.dockerContainers.sort(
(a, b) => (a.running ? 0 : 1) - (b.running ? 0 : 1) || a.name.localeCompare(b.name)
),
]}
emptyContent={<span className="text-orange-400">Docker service has no containers.</span>}
>
{({ id, image, name, running, created }) => (
<ListboxItem
key={id}
description={image}
startContent={running ? <>🟢</> : <>🔴</>}
endContent={
<span className="text-xs" title="Created">
{created}
</span>
}
>
{name}
</ListboxItem>
)}
</Listbox>
) : (
<span className="text-red-400">Docker service is not found.</span>
)}
</CardBody>
</Card>

<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<KVBox title="Systemd Services" kvs={host.systemdServices.map((x) => ({ key: x, value: '✅' }))} />

<KVBox title="Systemd Timers" kvs={host.systemdTimers.map((x) => ({ key: x, value: '✅' }))} />
</div>
</div>
);
}
38 changes: 38 additions & 0 deletions website/src/components/report/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { LhpHostReport } from '@/lib/data';
import { Listbox, ListboxItem } from '@nextui-org/listbox';
import Image from 'next/image';
import { getCloudIconName } from './helpers';

export interface SidebarProps {
data: LhpHostReport[];
onHostSelect: (host: LhpHostReport) => void;
}

export function Sidebar({ data, onHostSelect }: SidebarProps) {
return (
<Listbox aria-label="Sidebar" items={data}>
{/* @ts-ignore */}
{(host) => (
<ListboxItem key={host.host.name} onPress={() => onHostSelect(host)}>
<div className="flex items-center space-x-2">
<Image
src={`https://cdn.simpleicons.org/${getCloudIconName(host.cloud.name)}`}
width="16"
height="16"
alt={`logo ${host.cloud.name}`}
unoptimized
/>
<Image
src={`https://cdn.simpleicons.org/${host.distribution.id}`}
width="16"
height="16"
alt={`logo ${host.distribution.id}`}
unoptimized
/>
<span>{host.host.name}</span>
</div>
</ListboxItem>
)}
</Listbox>
);
}
Loading

0 comments on commit 9719020

Please sign in to comment.