Skip to content

Commit 242c1d3

Browse files
authored
feat: add sidecar management features (#515)
1 parent 6de9a60 commit 242c1d3

3 files changed

Lines changed: 276 additions & 45 deletions

File tree

packages/workshop-app/app/routes/admin+/admin-utils.server.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import path from 'node:path'
33
import { getWorkshopRoot } from '@epic-web/workshop-utils/apps.server'
44
import { deleteCache } from '@epic-web/workshop-utils/cache.server'
55
import { deleteDb } from '@epic-web/workshop-utils/db.server'
6+
import {
7+
getSidecarLogs,
8+
restartSidecarProcess,
9+
} from '@epic-web/workshop-utils/process-manager.server'
610
import fsExtra from 'fs-extra'
711

812
export function isInspectorRunning(): boolean {
@@ -13,6 +17,19 @@ export function isInspectorRunning(): boolean {
1317
}
1418
}
1519

20+
export async function restartSidecar(name: string): Promise<boolean> {
21+
if (ENV.EPICSHOP_DEPLOYED) return false
22+
return restartSidecarProcess(name)
23+
}
24+
25+
export function getSidecarLogLines(
26+
name: string,
27+
lineCount: number = 50,
28+
): string {
29+
if (ENV.EPICSHOP_DEPLOYED) return ''
30+
return getSidecarLogs(name, lineCount)
31+
}
32+
1633
export async function clearData() {
1734
if (ENV.EPICSHOP_DEPLOYED) return
1835
await clearCaches()

packages/workshop-app/app/routes/admin+/index.tsx

Lines changed: 151 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
} from '@epic-web/workshop-utils/timing.server'
77
import { type SEOHandle } from '@nasa-gcn/remix-seo'
88
import { clsx } from 'clsx'
9-
import { data, Form, Link, useNavigation } from 'react-router'
9+
import * as React from 'react'
10+
import { data, Form, Link, useFetcher, useNavigation } from 'react-router'
1011
import { Icon } from '#app/components/icons.tsx'
1112
import { SimpleTooltip } from '#app/components/ui/tooltip.tsx'
1213
import {
@@ -19,7 +20,9 @@ import { type Route } from './+types/index.tsx'
1920
import {
2021
clearCaches,
2122
clearData,
23+
getSidecarLogLines,
2224
isInspectorRunning,
25+
restartSidecar,
2326
startInspector,
2427
stopInspector,
2528
} from './admin-utils.server.tsx'
@@ -107,6 +110,22 @@ export async function action({ request }: Route.ActionArgs) {
107110
await stopInspector()
108111
return { success: true }
109112
}
113+
case 'restart-sidecar': {
114+
const name = formData.get('name')
115+
if (typeof name !== 'string') {
116+
throw new Error('Sidecar name is required')
117+
}
118+
const success = await restartSidecar(name)
119+
return { success }
120+
}
121+
case 'get-sidecar-logs': {
122+
const name = formData.get('name')
123+
if (typeof name !== 'string') {
124+
throw new Error('Sidecar name is required')
125+
}
126+
const logs = getSidecarLogLines(name, 1000)
127+
return { success: true, logs }
128+
}
110129
default: {
111130
throw new Error(`Unknown intent: ${intent}`)
112131
}
@@ -174,35 +193,14 @@ export default function AdminLayout({
174193
<CardDescription>Background sidecar processes</CardDescription>
175194
</CardHeader>
176195
<CardContent>
177-
<ul className="scrollbar-thin scrollbar-thumb-scrollbar flex max-h-48 flex-col gap-2 overflow-y-auto">
196+
<ul className="flex flex-col gap-2">
178197
{Object.entries(data.sidecarProcesses).map(([key, process]) => (
179-
<li
198+
<SidecarProcessItem
180199
key={key}
181-
className="border-border bg-muted/30 rounded-md border p-3"
182-
>
183-
<div className="flex items-center gap-2">
184-
{process.running ? (
185-
<Pinger status="running" />
186-
) : (
187-
<Pinger status="taken" />
188-
)}
189-
<span className="font-mono text-sm font-semibold">
190-
{key}
191-
</span>
192-
</div>
193-
<div className="text-muted-foreground mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs">
194-
{process.pid && (
195-
<span>
196-
<span className="font-medium">PID:</span>{' '}
197-
{process.pid}
198-
</span>
199-
)}
200-
<span>
201-
<span className="font-medium">Status:</span>{' '}
202-
{process.running ? 'Running' : 'Failed'}
203-
</span>
204-
</div>
205-
</li>
200+
name={key}
201+
pid={process.pid}
202+
running={process.running}
203+
/>
206204
))}
207205
</ul>
208206
</CardContent>
@@ -452,6 +450,131 @@ export default function AdminLayout({
452450
)
453451
}
454452

453+
function SidecarProcessItem({
454+
name,
455+
pid,
456+
running,
457+
}: {
458+
name: string
459+
pid?: number
460+
running: boolean
461+
}) {
462+
const restartFetcher = useFetcher()
463+
const logsFetcher = useFetcher<{ logs?: string }>()
464+
const [copyStatus, setCopyStatus] = React.useState<'idle' | 'copied'>('idle')
465+
const [logsOpen, setLogsOpen] = React.useState(false)
466+
const [displayLogs, setDisplayLogs] = React.useState<string | null>(null)
467+
468+
const isRestarting = restartFetcher.state !== 'idle'
469+
const isLoadingLogs = logsFetcher.state !== 'idle'
470+
471+
// Update display logs when fetcher returns data
472+
React.useEffect(() => {
473+
if (logsFetcher.data) {
474+
setDisplayLogs(logsFetcher.data.logs ?? '')
475+
}
476+
}, [logsFetcher.data])
477+
478+
const handleCopyLogs = () => {
479+
if (!displayLogs) return
480+
navigator.clipboard
481+
.writeText(displayLogs)
482+
.then(() => {
483+
setCopyStatus('copied')
484+
setTimeout(() => setCopyStatus('idle'), 2000)
485+
})
486+
.catch(() => {
487+
// silently fail
488+
})
489+
}
490+
491+
const handleToggleLogs = (open: boolean) => {
492+
setLogsOpen(open)
493+
if (open) {
494+
// Fetch logs when opening
495+
void logsFetcher.submit(
496+
{ intent: 'get-sidecar-logs', name },
497+
{ method: 'POST' },
498+
)
499+
}
500+
}
501+
502+
return (
503+
<li className="border-border bg-muted/30 rounded-md border p-3">
504+
<div className="flex items-center justify-between gap-2">
505+
<div className="flex items-center gap-2">
506+
{running ? <Pinger status="running" /> : <Pinger status="taken" />}
507+
<span className="font-mono text-sm font-semibold">{name}</span>
508+
</div>
509+
<div className="flex items-center gap-1">
510+
<restartFetcher.Form method="POST">
511+
<input type="hidden" name="intent" value="restart-sidecar" />
512+
<input type="hidden" name="name" value={name} />
513+
<SimpleTooltip content="Restart process">
514+
<button
515+
type="submit"
516+
disabled={isRestarting}
517+
className="text-muted-foreground hover:text-foreground hover:bg-muted rounded p-1.5 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
518+
>
519+
<Icon
520+
name="Refresh"
521+
className={cn('h-4 w-4', isRestarting && 'animate-spin')}
522+
/>
523+
</button>
524+
</SimpleTooltip>
525+
</restartFetcher.Form>
526+
</div>
527+
</div>
528+
<div className="text-muted-foreground mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs">
529+
{pid && (
530+
<span>
531+
<span className="font-medium">PID:</span> {pid}
532+
</span>
533+
)}
534+
<span>
535+
<span className="font-medium">Status:</span>{' '}
536+
{isRestarting ? 'Restarting...' : running ? 'Running' : 'Failed'}
537+
</span>
538+
</div>
539+
<details
540+
className="mt-3"
541+
open={logsOpen}
542+
onToggle={(e) => handleToggleLogs(e.currentTarget.open)}
543+
>
544+
<summary className="text-muted-foreground hover:text-foreground cursor-pointer text-xs font-medium">
545+
{isLoadingLogs ? 'Loading logs...' : 'View logs'}
546+
</summary>
547+
<div className="mt-2">
548+
<div className="mb-1 flex justify-end">
549+
<SimpleTooltip
550+
content={copyStatus === 'copied' ? 'Copied!' : 'Copy logs'}
551+
>
552+
<button
553+
type="button"
554+
onClick={handleCopyLogs}
555+
disabled={!displayLogs}
556+
className={cn(
557+
'text-muted-foreground hover:text-foreground hover:bg-muted rounded p-1 transition-colors disabled:cursor-not-allowed disabled:opacity-50',
558+
copyStatus === 'copied' && 'text-success',
559+
)}
560+
>
561+
<Icon
562+
name={copyStatus === 'copied' ? 'CheckSmall' : 'Copy'}
563+
className="h-3.5 w-3.5"
564+
/>
565+
</button>
566+
</SimpleTooltip>
567+
</div>
568+
<pre className="scrollbar-thin scrollbar-thumb-scrollbar bg-background max-h-96 overflow-auto rounded border p-2 text-xs">
569+
{displayLogs ||
570+
(isLoadingLogs ? 'Loading...' : 'No logs available')}
571+
</pre>
572+
</div>
573+
</details>
574+
</li>
575+
)
576+
}
577+
455578
function Card({
456579
children,
457580
className,

0 commit comments

Comments
 (0)