generated from vst/haskell-template-hebele
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(website): split report into tabs, refactor report module
- Loading branch information
Showing
10 changed files
with
341 additions
and
288 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
Oops, something went wrong.