- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift) - LogLift evidence pipeline (migration 078): upload webhook, B2 storage client, receiver/matcher, EventLogCollector PowerShell script - IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket xrefs, applications/configurations browse pages + apply/revert/audit endpoints - Link-aware analyzer bundles (migration 073) + provider toggle (migration 074): link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion panels, analyze-bundle endpoint - Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts admin page, reconciler service, resolve endpoints - Dashboard overhaul: integration-health service + alerts, overview/health endpoints - Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
189 lines
5.8 KiB
TypeScript
189 lines
5.8 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Per-row "Run RMM" dialog. Lists asset-self scripts from /api/rmm/scripts,
|
|
* dispatches against a known Datto deviceUid, and surfaces live execution
|
|
* status via the existing RmmExecutionStream — without leaving the page.
|
|
*
|
|
* Used from /configuration-items so admins don't have to construct hidden
|
|
* /analyzer/itglue/configurations/<id> URLs by hand.
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import { Loader2, Terminal } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { useSession } from '@/lib/auth-client';
|
|
import { RmmExecutionStream } from './rmm-execution-stream';
|
|
|
|
interface Script {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
target_type: 'site_anchor' | 'asset_self';
|
|
expected_runtime_seconds: number;
|
|
version: number;
|
|
}
|
|
|
|
interface RmmDispatchDialogProps {
|
|
deviceUid: string;
|
|
hostname?: string | null;
|
|
companyId?: number | string | null;
|
|
triggerLabel?: string;
|
|
}
|
|
|
|
export function RmmDispatchDialog({
|
|
deviceUid,
|
|
hostname,
|
|
companyId,
|
|
triggerLabel = 'Run RMM',
|
|
}: RmmDispatchDialogProps) {
|
|
const { data: session } = useSession();
|
|
const role = (session?.user as { role?: string } | undefined)?.role ?? 'user';
|
|
const canExecute = role === 'admin' || role === 'super-admin';
|
|
|
|
const [scripts, setScripts] = useState<Script[] | null>(null);
|
|
const [open, setOpen] = useState(false);
|
|
const [running, setRunning] = useState<string | null>(null);
|
|
const [activeExecutionId, setActiveExecutionId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open || scripts !== null) return;
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const res = await fetch('/api/rmm/scripts');
|
|
if (!res.ok) return;
|
|
const data = (await res.json()) as { scripts: Script[] };
|
|
if (!cancelled) setScripts(data.scripts);
|
|
} catch {
|
|
// Silent — the dialog just won't populate.
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [open, scripts]);
|
|
|
|
async function dispatch(s: Script): Promise<void> {
|
|
if (!canExecute) return;
|
|
setRunning(s.id);
|
|
try {
|
|
const res = await fetch('/api/rmm/executions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
scriptId: s.id,
|
|
target: {
|
|
type: 'asset_self',
|
|
deviceUid,
|
|
hostname: hostname ?? null,
|
|
companyId: companyId ?? null,
|
|
},
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
throw new Error(data.message ?? data.error ?? 'Execution failed');
|
|
}
|
|
setActiveExecutionId(data.executionId);
|
|
toast.success(`${s.name}: queued`);
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : 'Could not dispatch');
|
|
} finally {
|
|
setRunning(null);
|
|
}
|
|
}
|
|
|
|
const visible = scripts?.filter((s) => s.target_type === 'asset_self') ?? [];
|
|
const disabledReason = !canExecute
|
|
? 'Requires admin'
|
|
: !deviceUid
|
|
? 'No Datto device id'
|
|
: null;
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(o) => {
|
|
setOpen(o);
|
|
// Reset active execution when the dialog is closed so the next open
|
|
// starts fresh. Status stays visible until the user closes.
|
|
if (!o) setActiveExecutionId(null);
|
|
}}
|
|
>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={!!disabledReason}
|
|
title={disabledReason ?? 'Dispatch a Datto RMM script for this device'}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Terminal className="w-3.5 h-3.5 mr-1.5" />
|
|
{triggerLabel}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent
|
|
className="max-w-2xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>Dispatch RMM Script</DialogTitle>
|
|
<DialogDescription>
|
|
Target: <span className="font-mono">{hostname ?? deviceUid}</span>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{visible.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground py-2">
|
|
{scripts === null ? 'Loading…' : 'No asset-targeted scripts in the registry.'}
|
|
</p>
|
|
) : (
|
|
<ul className="divide-y border rounded-md max-h-[40vh] overflow-auto">
|
|
{visible.map((s) => (
|
|
<li key={s.id}>
|
|
<button
|
|
type="button"
|
|
className="w-full text-left px-3 py-2 hover:bg-accent flex items-start gap-2 disabled:opacity-50"
|
|
onClick={() => dispatch(s)}
|
|
disabled={running !== null || activeExecutionId !== null}
|
|
>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="text-sm font-medium flex items-center gap-2">
|
|
{s.name}
|
|
<Badge variant="outline" className="text-[10px] py-0">
|
|
~{s.expected_runtime_seconds}s
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
{s.description}
|
|
</p>
|
|
</div>
|
|
{running === s.id && (
|
|
<Loader2 className="w-3.5 h-3.5 animate-spin shrink-0" />
|
|
)}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{activeExecutionId && (
|
|
<div className="mt-2">
|
|
<RmmExecutionStream executionId={activeExecutionId} />
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|