diff --git a/website/src/app/report/page.tsx b/website/src/app/report/page.tsx index 8deebfa..79d0bef 100644 --- a/website/src/app/report/page.tsx +++ b/website/src/app/report/page.tsx @@ -1,5 +1,5 @@ -import { AppMain } from '@/components/app'; +import { Report } from '@/components/report'; export default function Page() { - return ; + return ; } diff --git a/website/src/components/app/-ui.tsx b/website/src/components/helpers.tsx similarity index 100% rename from website/src/components/app/-ui.tsx rename to website/src/components/helpers.tsx diff --git a/website/src/components/report/App.tsx b/website/src/components/report/App.tsx new file mode 100644 index 0000000..93e9c89 --- /dev/null +++ b/website/src/components/report/App.tsx @@ -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>(Nothing); + type TabKey = 'overview' | 'tabulate-hosts' | 'show-host-details' | 'flush'; + const [tab, setTab] = useState('overview'); + + useEffect(() => { + if (host.isJust()) { + setTab('show-host-details'); + } + }, [host]); + + return ( +
+ { + if (x === 'flush') { + onFlushRequest(); + } else { + setTab(x as TabKey); + } + }} + > + + + + + + + + + + + + + + +
+ ); +} + +export function TabOverview() { + return
Overview is coming soon...
; +} + +export function TabTabulateHosts({ + data, + setHost, +}: { + data: LhpPatrolReport; + setHost: (x: Maybe) => void; +}) { + return setHost(Just(x))} />; +} + +export function TabShowHostDetails({ + data, + host, + setHost, +}: { + data: LhpPatrolReport; + host: Maybe; + setHost: (x: Maybe) => void; +}) { + return ( +
+
+ setHost(Just(x))} /> +
+ +
+ {host.caseOf({ + Nothing: () =>
Choose a host to view details.
, + Just: (x) => , + })} +
+
+ ); +} diff --git a/website/src/components/report/DataLoader.tsx b/website/src/components/report/DataLoader.tsx new file mode 100644 index 0000000..6716216 --- /dev/null +++ b/website/src/components/report/DataLoader.tsx @@ -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(); + + const changeHandler = (e: ChangeEvent) => { + 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 ( + + + Load Data + + + + + + + + {error && ( + +

{error}

+
+ )} +
+
+ ); +} diff --git a/website/src/components/report/ShowHostDetails.tsx b/website/src/components/report/ShowHostDetails.tsx new file mode 100644 index 0000000..f827df7 --- /dev/null +++ b/website/src/components/report/ShowHostDetails.tsx @@ -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 ( +
+

+
+ {host.host.name} + {host.host.url && ( + + 🔗 + + )} +
+ +
+ {(host.host.tags || []).map((x) => ( + + {x} + + ))} +
+

+ + + +
+ + + +
+ + + Authorized SSH Keys + + + No authorized SSH keys are found. Sounds weird?} + > + {({ length, type, fingerprint, data, comment }) => ( + { + navigator.clipboard.writeText(data); + toast('SSH Key is copied to clipboard.'); + }} + > + {`${type} (${length}) - ${fingerprint} - ${comment || ''}`} + + )} + + + + + + Docker Containers + + + {host.dockerContainers ? ( + (a.running ? 0 : 1) - (b.running ? 0 : 1) || a.name.localeCompare(b.name) + ), + ]} + emptyContent={Docker service has no containers.} + > + {({ id, image, name, running, created }) => ( + 🟢 : <>🔴} + endContent={ + + {created} + + } + > + {name} + + )} + + ) : ( + Docker service is not found. + )} + + + +
+ ({ key: x, value: '✅' }))} /> + + ({ key: x, value: '✅' }))} /> +
+
+ ); +} diff --git a/website/src/components/report/Sidebar.tsx b/website/src/components/report/Sidebar.tsx new file mode 100644 index 0000000..5aa3cb1 --- /dev/null +++ b/website/src/components/report/Sidebar.tsx @@ -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 ( + + {/* @ts-ignore */} + {(host) => ( + onHostSelect(host)}> +
+ {`logo + {`logo + {host.host.name} +
+
+ )} +
+ ); +} diff --git a/website/src/components/app/-app.tsx b/website/src/components/report/TabulateHosts.tsx similarity index 62% rename from website/src/components/app/-app.tsx rename to website/src/components/report/TabulateHosts.tsx index 2ae8c88..055a74a 100644 --- a/website/src/components/app/-app.tsx +++ b/website/src/components/report/TabulateHosts.tsx @@ -1,104 +1,11 @@ -import { Card, CardBody, CardHeader } from '@nextui-org/card'; +import { LhpHostReport } from '@/lib/data'; import { Chip } from '@nextui-org/chip'; -import { Listbox, ListboxItem, ListboxSection } from '@nextui-org/listbox'; import { Radio, RadioGroup, Select, SelectItem, Selection, Slider } from '@nextui-org/react'; import { Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/table'; import Image from 'next/image'; import Link from 'next/link'; -import { Just, Maybe, Nothing } from 'purify-ts/Maybe'; import { useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; -import { LhpHostReport, LhpPatrolReport } from './-data'; -import { KVBox } from './-ui'; - -export function App({ data, onFlushRequest }: { data: LhpPatrolReport; onFlushRequest: () => void }) { - const [host, setHost] = useState>(Nothing); - - return ( -
-
- setHost(Just(x))} - onFlushRequest={onFlushRequest} - onTabulateRequest={() => { - setHost(Nothing); - }} - /> -
- -
- {host.caseOf({ - Nothing: () => setHost(Just(x))} />, - Just: (x) => , - })} -
-
- ); -} - -export interface SidebarProps { - data: LhpHostReport[]; - onHostSelect: (host: LhpHostReport) => void; - onFlushRequest: () => void; - onTabulateRequest: () => void; -} - -export function Sidebar({ data, onHostSelect, onTabulateRequest, onFlushRequest }: SidebarProps) { - return ( - - - onTabulateRequest()}> - Tabulate - - - - - {/* @ts-ignore */} - {(host) => ( - onHostSelect(host)}> -
- {`logo - {`logo - {host.host.name} -
-
- )} -
- - - onFlushRequest()}> - Flush Data - - -
- ); -} - -export function cloudIcon(x: string) { - switch (x.toLowerCase()) { - case 'aws': - return 'amazonec2'; - case 'do': - return 'digitalocean'; - case 'hetzner': - return 'hetzner'; - default: - return 'educative'; - } -} +import { getCloudIconName } from './helpers'; export function TabulateHosts({ hosts, @@ -420,7 +327,7 @@ export function TabulateHosts({
{`logo ); } - -export function HostDetails({ host }: { host: LhpHostReport }) { - return ( -
-

-
- {host.host.name} - {host.host.url && ( - - 🔗 - - )} -
- -
- {(host.host.tags || []).map((x) => ( - - {x} - - ))} -
-

- -
- -
- -
- - - -
- -
- - Authorized SSH Keys - - - No authorized SSH keys are found. Sounds weird?} - > - {({ length, type, fingerprint, data, comment }) => ( - { - navigator.clipboard.writeText(data); - toast('SSH Key is copied to clipboard.'); - }} - > - {`${type} (${length}) - ${fingerprint} - ${comment || ''}`} - - )} - - - -
- -
- - Docker Containers - - - {host.dockerContainers ? ( - (a.running ? 0 : 1) - (b.running ? 0 : 1) || a.name.localeCompare(b.name) - ), - ]} - emptyContent={Docker service has no containers.} - > - {({ id, image, name, running, created }) => ( - 🟢 : <>🔴} - endContent={ - - {created} - - } - > - {name} - - )} - - ) : ( - Docker service is not found. - )} - - -
- -
- ({ key: x, value: '✅' }))} /> - - ({ key: x, value: '✅' }))} /> -
-
- ); -} diff --git a/website/src/components/report/helpers.tsx b/website/src/components/report/helpers.tsx new file mode 100644 index 0000000..25d0ba9 --- /dev/null +++ b/website/src/components/report/helpers.tsx @@ -0,0 +1,12 @@ +export function getCloudIconName(x: string) { + switch (x.toLowerCase()) { + case 'aws': + return 'amazonec2'; + case 'do': + return 'digitalocean'; + case 'hetzner': + return 'hetzner'; + default: + return 'educative'; + } +} diff --git a/website/src/components/app/index.tsx b/website/src/components/report/index.tsx similarity index 81% rename from website/src/components/app/index.tsx rename to website/src/components/report/index.tsx index 9f02e39..0e27e1f 100644 --- a/website/src/components/app/index.tsx +++ b/website/src/components/report/index.tsx @@ -1,12 +1,13 @@ 'use client'; +import { LhpPatrolReport, deleteData, loadData } from '@/lib/data'; import { Just, Maybe, Nothing } from 'purify-ts/Maybe'; import { useEffect, useState } from 'react'; -import { App } from './-app'; -import { DataLoader, LhpPatrolReport, deleteData, loadData } from './-data'; -import { BigSpinner } from './-ui'; +import { BigSpinner } from '../helpers'; +import { App } from './App'; +import { DataLoader } from './DataLoader'; -export function AppMain() { +export function Report() { const [data, setAppData] = useState>>(Nothing); useEffect(() => { diff --git a/website/src/components/app/-data.tsx b/website/src/lib/data.ts similarity index 88% rename from website/src/components/app/-data.tsx rename to website/src/lib/data.ts index 619989d..d3bbbd7 100644 --- a/website/src/components/app/-data.tsx +++ b/website/src/lib/data.ts @@ -1,12 +1,8 @@ -import { Card, CardBody, CardFooter, CardHeader } from '@nextui-org/card'; -import { Divider } from '@nextui-org/divider'; import Ajv from 'ajv'; import type { JSONSchema } from 'json-schema-to-ts'; import { FromSchema } from 'json-schema-to-ts'; import { Either, Left, Right } from 'purify-ts/Either'; import { Just, Maybe, Nothing } from 'purify-ts/Maybe'; -import { ChangeEvent, useState } from 'react'; -import { Centered } from './-ui'; export const LHP_PATROL_REPORT_SCHEMA = { $comment: 'Host Patrol Report\nReport', @@ -255,48 +251,3 @@ export function saveData(x: LhpPatrolReport): void { export function deleteData(): void { localStorage.removeItem(_LOCAL_STORAGE_KEY_DATA); } - -export function DataLoader({ onLoadData }: { onLoadData: (x: LhpPatrolReport) => void }) { - const [error, setError] = useState(); - - const changeHandler = (e: ChangeEvent) => { - 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 ( - - - Load Data - - - - - - - - {error && ( - -

{error}

-
- )} -
-
- ); -}