diff --git a/src/webui/src/components/react/index.tsx b/src/webui/src/components/react/index.tsx index 9a9b11e..1abebcb 100644 --- a/src/webui/src/components/react/index.tsx +++ b/src/webui/src/components/react/index.tsx @@ -1,139 +1,139 @@ import { api } from '@/api'; import Rename from '@/components/react/rename'; import { useEffect, useState, Fragment } from 'react'; import { Menu, Transition } from '@headlessui/react'; import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'; const Index = (props: { base: string }) => { const [items, setItems] = useState([]); const badge = { online: 'bg-emerald-400', stopped: 'bg-red-500', crashed: 'bg-amber-400', }; async function fetch() { const items = await api.get(props.base + '/list').json(); const servers = await api.get(props.base + '/daemon/servers').json(); setItems(items.map((s) => ({ ...s, server: 'Internal' }))); await servers.forEach(async (name) => { const remote = await api.get(props.base + `/remote/${name}/list`).json(); setItems((s) => [...s, ...remote.map((i) => ({ ...i, server: name }))]); }); } const classNames = (...classes: Array) => classes.filter(Boolean).join(' '); const isRemote = (item: any): bool => (item.server == 'Internal' ? false : true); const isRunning = (status: string): bool => (status == 'stopped' ? false : status == 'crashed' ? false : true); const action = (id: number, name: string) => api.post(`${props.base}/process/${id}/action`, { json: { method: name } }).then(() => fetch()); useEffect(() => fetch(), []); return ( ); }; export default Index; diff --git a/src/webui/src/components/react/view.tsx b/src/webui/src/components/react/view.tsx index 0b17e5b..8492230 100644 --- a/src/webui/src/components/react/view.tsx +++ b/src/webui/src/components/react/view.tsx @@ -1,315 +1,317 @@ import { api } from '@/api'; import { matchSorter } from 'match-sorter'; import Rename from '@/components/react/rename'; import { Menu, Transition } from '@headlessui/react'; import { useEffect, useState, useRef, Fragment } from 'react'; import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'; const classNames = (...classes: Array) => classes.filter(Boolean).join(' '); const formatMemory = (bytes: number): [number, string] => { const units = ['b', 'kb', 'mb', 'gb']; let size = bytes; let unitIndex = 0; while (size > 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return [+size.toFixed(1), units[unitIndex]]; }; const startDuration = (input: string): [number, string] => { const matches = input.match(/(\d+)([dhms])/); if (matches) { const value = parseInt(matches[1], 10); const unit = matches[2]; return [value, unit]; } return null; }; const LogRow = ({ match, children }: any) => { const _match = match.toLowerCase(); const chunks = match.length ? children.split(new RegExp('(' + match + ')', 'ig')) : [children]; return (
{chunks.map((chunk: any, index: number) => chunk.toLowerCase() === _match ? ( {chunk} ) : ( {chunk} ) )}
); }; const LogViewer = (props: { server: string | null; base: string; id: number }) => { const [logs, setLogs] = useState([]); const [loaded, setLoaded] = useState(false); const lastRow = useRef(null); const [searchQuery, setSearchQuery] = useState(''); const [searchOpen, setSearchOpen] = useState(false); const [componentHeight, setComponentHeight] = useState(0); const filtered = (!searchQuery && logs) || matchSorter(logs, searchQuery); useEffect(() => { const updateComponentHeight = () => { const windowHeight = window.innerHeight; const newHeight = (windowHeight * 4) / 6; setComponentHeight(newHeight); }; updateComponentHeight(); window.addEventListener('resize', updateComponentHeight); return () => { window.removeEventListener('resize', updateComponentHeight); }; }, []); const componentStyle = { height: componentHeight + 'px', }; useEffect(() => { const handleKeydown = (event: any) => { if ((event.ctrlKey || event.metaKey) && event.key === 'f') { setSearchOpen(true); event.preventDefault(); } }; const handleKeyup = (event: any) => { if (event.key === 'Escape') { setSearchQuery(''); setSearchOpen(false); } }; const handleClick = () => { setSearchQuery(''); setSearchOpen(false); }; window.addEventListener('click', handleClick); window.addEventListener('keydown', handleKeydown); window.addEventListener('keyup', handleKeyup); return () => { window.removeEventListener('click', handleClick); window.removeEventListener('keydown', handleKeydown); window.removeEventListener('keyup', handleKeyup); }; }, [searchOpen]); const loadLogs = () => { api .get(`${props.base}/process/${props.id}/logs/out`) .json() .then((data) => setLogs(data.logs)) .finally(() => setLoaded(true)); }; const loadLogsRemote = () => { api .get(`${props.base}/remote/${props.server}/logs/${props.id}/out`) .json() .then((data) => setLogs(data.logs)) .finally(() => setLoaded(true)); }; useEffect(() => (props.server != null ? loadLogsRemote() : loadLogs()), []); useEffect(() => lastRow.current?.scrollIntoView(), [loaded]); if (!loaded) { return
loading...
; } else { return (
{searchOpen && (
setSearchQuery(e.target.value)} /> {searchQuery && filtered.length + ' matches'}
)}
{filtered.map((log, index) => ( {log} ))}
); } }; const View = (props: { id: string; base: string }) => { const [item, setItem] = useState(); const [loaded, setLoaded] = useState(false); const server = new URLSearchParams(window.location.search).get('server'); const badge = { online: 'bg-emerald-400/10 text-emerald-400', stopped: 'bg-red-500/10 text-red-500', crashed: 'bg-amber-400/10 text-amber-400', }; const fetch = () => { api .get(`${props.base}/process/${props.id}/info`) .json() .then((res) => setItem(res)) .finally(() => setLoaded(true)); }; const fetchRemote = () => { api .get(`${props.base}/remote/${server}/info/${props.id}`) .json() .then((res) => setItem(res)) .finally(() => setLoaded(true)); }; const isRunning = (status: string): bool => (status == 'stopped' ? false : status == 'crashed' ? false : true); const action = (id: number, name: string) => api.post(`${props.base}/process/${id}/action`, { json: { method: name } }).then(() => fetch()); useEffect(() => (server != null ? fetchRemote() : fetch()), []); if (!loaded) { return
loading...
; } else { const online = isRunning(item.info.status); const [uptime, upunit] = startDuration(item.info.uptime); const [memory, memunit] = formatMemory(online ? item.stats.memory_usage.rss : 0); const stats = [ { name: 'Status', value: item.info.status }, { name: 'Uptime', value: online ? uptime : 'none', unit: online ? upunit : '' }, { name: 'Memory', value: online ? memory : 'offline', unit: online ? memunit : '' }, { name: 'CPU', value: online ? item.stats.cpu_percent : 'offline', unit: online ? '%' : '' }, ]; return (

- {item.info.name} + {server != null ? `${server}/${item.info.name}` : item.info.name}

-
- {online ? item.info.pid : 'none'} -
+ {online && ( +
+ {item.info.pid} +
+ )}

{item.info.command}

{stats.map((stat: any, index: number) => (

{stat.name}

{stat.value} {stat.unit ? {stat.unit} : null}

))}
); } }; export default View;