mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 16:53:38 +02:00
Show last error status on integrations. Closes #122.
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef, lazy, Suspense } from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { ChevronDown, Info } from 'lucide-react';
|
||||
import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Button } from '../ui/button';
|
||||
import { Separator } from '../ui/separator';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '../ui/dialog';
|
||||
import { toast } from '../ui/sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { api } from '../../api';
|
||||
@@ -1854,6 +1861,10 @@ export function SettingsFanoutSection({
|
||||
const [inlineEditName, setInlineEditName] = useState('');
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [selectedCreateType, setSelectedCreateType] = useState<DraftType | null>(null);
|
||||
const [errorDialogState, setErrorDialogState] = useState<{
|
||||
integrationName: string;
|
||||
error: string;
|
||||
} | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const loadConfigs = useCallback(async () => {
|
||||
@@ -2207,6 +2218,33 @@ export function SettingsFanoutSection({
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={errorDialogState !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setErrorDialogState(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader className="border-b border-border px-5 py-4">
|
||||
<DialogTitle>
|
||||
{errorDialogState
|
||||
? `${errorDialogState.integrationName} Error`
|
||||
: 'Integration Error'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Most recent backend error retained for this integration.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="px-5 py-4 text-sm text-muted-foreground">
|
||||
<p className="whitespace-pre-wrap break-words font-mono text-foreground">
|
||||
{errorDialogState?.error}
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{configGroups.length > 0 && (
|
||||
<div className="columns-1 gap-4 md:columns-2">
|
||||
{configGroups.map((group) => (
|
||||
@@ -2220,6 +2258,7 @@ export function SettingsFanoutSection({
|
||||
{group.configs.map((cfg) => {
|
||||
const statusEntry = health?.fanout_statuses?.[cfg.id];
|
||||
const status = cfg.enabled ? statusEntry?.status : undefined;
|
||||
const lastError = cfg.enabled ? statusEntry?.last_error : null;
|
||||
const communityConfig = cfg.config as Record<string, unknown>;
|
||||
return (
|
||||
<div
|
||||
@@ -2286,6 +2325,25 @@ export function SettingsFanoutSection({
|
||||
{cfg.enabled ? getStatusLabel(status, cfg.type) : 'Disabled'}
|
||||
</span>
|
||||
|
||||
{lastError && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 px-0"
|
||||
onClick={() =>
|
||||
setErrorDialogState({
|
||||
integrationName: cfg.name,
|
||||
error: lastError,
|
||||
})
|
||||
}
|
||||
aria-label={`View error details for ${cfg.name}`}
|
||||
title="View latest error"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
|
||||
@@ -206,6 +206,56 @@ describe('SettingsFanoutSection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows an error info button and dialog when the integration has a retained error', async () => {
|
||||
mockedApi.getFanoutConfigs.mockResolvedValue([webhookConfig]);
|
||||
renderSection({
|
||||
health: {
|
||||
...baseHealth,
|
||||
fanout_statuses: {
|
||||
'wh-1': {
|
||||
name: 'Test Hook',
|
||||
type: 'webhook',
|
||||
status: 'error',
|
||||
last_error: 'HTTP 500',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Hook')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'View error details for Test Hook' }));
|
||||
|
||||
expect(screen.getByRole('dialog', { name: 'Test Hook Error' })).toBeInTheDocument();
|
||||
expect(screen.getByText('HTTP 500')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show an error info button when the integration has no retained error', async () => {
|
||||
mockedApi.getFanoutConfigs.mockResolvedValue([webhookConfig]);
|
||||
renderSection({
|
||||
health: {
|
||||
...baseHealth,
|
||||
fanout_statuses: {
|
||||
'wh-1': {
|
||||
name: 'Test Hook',
|
||||
type: 'webhook',
|
||||
status: 'connected',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Hook')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'View error details for Test Hook' })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to edit view when clicking edit', async () => {
|
||||
mockedApi.getFanoutConfigs.mockResolvedValue([webhookConfig]);
|
||||
renderSection();
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface FanoutStatusEntry {
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
last_error?: string | null;
|
||||
}
|
||||
|
||||
export interface AppInfo {
|
||||
|
||||
Reference in New Issue
Block a user