Interleave failed runs with issues and approvals in inbox
Failed runs are no longer shown in a separate section. They are now mixed into the main work items feed sorted by timestamp, matching how approvals are already interleaved with issues. Replaced the large FailedRunCard with a compact FailedRunInboxRow that matches the ApprovalInboxRow visual style. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
72a0e256a8
commit
25af0a1532
3 changed files with 140 additions and 153 deletions
|
|
@ -289,7 +289,11 @@ describe("inbox helpers", () => {
|
||||||
getInboxWorkItems({
|
getInboxWorkItems({
|
||||||
issues: [olderIssue, newerIssue],
|
issues: [olderIssue, newerIssue],
|
||||||
approvals: [approval],
|
approvals: [approval],
|
||||||
}).map((item) => item.kind === "issue" ? `issue:${item.issue.id}` : `approval:${item.approval.id}`),
|
}).map((item) => {
|
||||||
|
if (item.kind === "issue") return `issue:${item.issue.id}`;
|
||||||
|
if (item.kind === "approval") return `approval:${item.approval.id}`;
|
||||||
|
return `run:${item.run.id}`;
|
||||||
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
"issue:1",
|
"issue:1",
|
||||||
"approval:approval-between",
|
"approval:approval-between",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ export type InboxWorkItem =
|
||||||
kind: "approval";
|
kind: "approval";
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
approval: Approval;
|
approval: Approval;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "failed_run";
|
||||||
|
timestamp: number;
|
||||||
|
run: HeartbeatRun;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface InboxBadgeData {
|
export interface InboxBadgeData {
|
||||||
|
|
@ -146,9 +151,11 @@ export function approvalActivityTimestamp(approval: Approval): number {
|
||||||
export function getInboxWorkItems({
|
export function getInboxWorkItems({
|
||||||
issues,
|
issues,
|
||||||
approvals,
|
approvals,
|
||||||
|
failedRuns = [],
|
||||||
}: {
|
}: {
|
||||||
issues: Issue[];
|
issues: Issue[];
|
||||||
approvals: Approval[];
|
approvals: Approval[];
|
||||||
|
failedRuns?: HeartbeatRun[];
|
||||||
}): InboxWorkItem[] {
|
}): InboxWorkItem[] {
|
||||||
return [
|
return [
|
||||||
...issues.map((issue) => ({
|
...issues.map((issue) => ({
|
||||||
|
|
@ -161,6 +168,11 @@ export function getInboxWorkItems({
|
||||||
timestamp: approvalActivityTimestamp(approval),
|
timestamp: approvalActivityTimestamp(approval),
|
||||||
approval,
|
approval,
|
||||||
})),
|
})),
|
||||||
|
...failedRuns.map((run) => ({
|
||||||
|
kind: "failed_run" as const,
|
||||||
|
timestamp: normalizeTimestamp(run.createdAt),
|
||||||
|
run,
|
||||||
|
})),
|
||||||
].sort((a, b) => {
|
].sort((a, b) => {
|
||||||
const timestampDiff = b.timestamp - a.timestamp;
|
const timestampDiff = b.timestamp - a.timestamp;
|
||||||
if (timestampDiff !== 0) return timestampDiff;
|
if (timestampDiff !== 0) return timestampDiff;
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,10 @@ import {
|
||||||
import {
|
import {
|
||||||
Inbox as InboxIcon,
|
Inbox as InboxIcon,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ArrowUpRight,
|
|
||||||
XCircle,
|
XCircle,
|
||||||
X,
|
X,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Identity } from "../components/Identity";
|
|
||||||
import { PageTabBar } from "../components/PageTabBar";
|
import { PageTabBar } from "../components/PageTabBar";
|
||||||
import type { Approval, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
|
import type { Approval, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/shared";
|
||||||
import {
|
import {
|
||||||
|
|
@ -64,16 +62,8 @@ type InboxCategoryFilter =
|
||||||
type SectionKey =
|
type SectionKey =
|
||||||
| "work_items"
|
| "work_items"
|
||||||
| "join_requests"
|
| "join_requests"
|
||||||
| "failed_runs"
|
|
||||||
| "alerts";
|
| "alerts";
|
||||||
|
|
||||||
const RUN_SOURCE_LABELS: Record<string, string> = {
|
|
||||||
timer: "Scheduled",
|
|
||||||
assignment: "Assignment",
|
|
||||||
on_demand: "Manual",
|
|
||||||
automation: "Automation",
|
|
||||||
};
|
|
||||||
|
|
||||||
function firstNonEmptyLine(value: string | null | undefined): string | null {
|
function firstNonEmptyLine(value: string | null | undefined): string | null {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean);
|
const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean);
|
||||||
|
|
@ -101,139 +91,102 @@ function readIssueIdFromRun(run: HeartbeatRun): string | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FailedRunCard({
|
function FailedRunInboxRow({
|
||||||
run,
|
run,
|
||||||
issueById,
|
issueById,
|
||||||
agentName: linkedAgentName,
|
agentName: linkedAgentName,
|
||||||
issueLinkState,
|
issueLinkState,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
onRetry,
|
||||||
|
isRetrying,
|
||||||
}: {
|
}: {
|
||||||
run: HeartbeatRun;
|
run: HeartbeatRun;
|
||||||
issueById: Map<string, Issue>;
|
issueById: Map<string, Issue>;
|
||||||
agentName: string | null;
|
agentName: string | null;
|
||||||
issueLinkState: unknown;
|
issueLinkState: unknown;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
|
onRetry: () => void;
|
||||||
|
isRetrying: boolean;
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const issueId = readIssueIdFromRun(run);
|
const issueId = readIssueIdFromRun(run);
|
||||||
const issue = issueId ? issueById.get(issueId) ?? null : null;
|
const issue = issueId ? issueById.get(issueId) ?? null : null;
|
||||||
const sourceLabel = RUN_SOURCE_LABELS[run.invocationSource] ?? "Manual";
|
|
||||||
const displayError = runFailureMessage(run);
|
const displayError = runFailureMessage(run);
|
||||||
|
|
||||||
const retryRun = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
const payload: Record<string, unknown> = {};
|
|
||||||
const context = run.contextSnapshot as Record<string, unknown> | null;
|
|
||||||
if (context) {
|
|
||||||
if (typeof context.issueId === "string" && context.issueId) payload.issueId = context.issueId;
|
|
||||||
if (typeof context.taskId === "string" && context.taskId) payload.taskId = context.taskId;
|
|
||||||
if (typeof context.taskKey === "string" && context.taskKey) payload.taskKey = context.taskKey;
|
|
||||||
}
|
|
||||||
const result = await agentsApi.wakeup(run.agentId, {
|
|
||||||
source: "on_demand",
|
|
||||||
triggerDetail: "manual",
|
|
||||||
reason: "retry_failed_run",
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
if (!("id" in result)) {
|
|
||||||
throw new Error("Retry was skipped because the agent is not currently invokable.");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
onSuccess: (newRun) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(run.companyId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(run.companyId, run.agentId) });
|
|
||||||
navigate(`/agents/${run.agentId}/runs/${newRun.id}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative overflow-hidden rounded-xl border border-red-500/30 bg-gradient-to-br from-red-500/10 via-card to-card p-4">
|
<div className="group border-b border-border px-2 py-2.5 last:border-b-0 sm:px-1 sm:pr-3 sm:py-2">
|
||||||
<div className="absolute right-0 top-0 h-24 w-24 rounded-full bg-red-500/10 blur-2xl" />
|
<div className="flex items-start gap-2 sm:items-center">
|
||||||
<button
|
<Link
|
||||||
type="button"
|
to={`/agents/${run.agentId}/runs/${run.id}`}
|
||||||
onClick={onDismiss}
|
className="flex min-w-0 flex-1 items-start gap-2 no-underline text-inherit transition-colors hover:bg-accent/50"
|
||||||
className="absolute right-2 top-2 z-10 rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100"
|
>
|
||||||
aria-label="Dismiss"
|
<span className="hidden h-2 w-2 shrink-0 sm:inline-flex" aria-hidden="true" />
|
||||||
>
|
<span className="hidden h-3.5 w-3.5 shrink-0 sm:inline-flex" aria-hidden="true" />
|
||||||
<X className="h-4 w-4" />
|
<span className="mt-0.5 shrink-0 rounded-md bg-red-500/20 p-1.5 sm:mt-0">
|
||||||
</button>
|
<XCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
||||||
<div className="relative space-y-3">
|
|
||||||
{issue ? (
|
|
||||||
<Link
|
|
||||||
to={`/issues/${issue.identifier ?? issue.id}`}
|
|
||||||
state={issueLinkState}
|
|
||||||
className="block truncate text-sm font-medium transition-colors hover:text-foreground no-underline text-inherit"
|
|
||||||
>
|
|
||||||
<span className="font-mono text-muted-foreground mr-1.5">
|
|
||||||
{issue.identifier ?? issue.id.slice(0, 8)}
|
|
||||||
</span>
|
|
||||||
{issue.title}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span className="block text-sm text-muted-foreground">
|
|
||||||
{run.errorCode ? `Error code: ${run.errorCode}` : "No linked issue"}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="line-clamp-2 text-sm font-medium sm:truncate sm:line-clamp-none">
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
{issue ? (
|
||||||
<div className="min-w-0 flex-1">
|
<>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<span className="font-mono text-muted-foreground mr-1.5">
|
||||||
<span className="rounded-md bg-red-500/20 p-1.5">
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
||||||
<XCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
</span>
|
||||||
</span>
|
{issue.title}
|
||||||
{linkedAgentName ? (
|
</>
|
||||||
<Identity name={linkedAgentName} size="sm" />
|
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm font-medium">Agent {run.agentId.slice(0, 8)}</span>
|
<>Failed run{linkedAgentName ? ` — ${linkedAgentName}` : ""}</>
|
||||||
)}
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
|
||||||
<StatusBadge status={run.status} />
|
<StatusBadge status={run.status} />
|
||||||
</div>
|
{linkedAgentName && issue ? <span>{linkedAgentName}</span> : null}
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<span className="truncate max-w-[300px]">{displayError}</span>
|
||||||
{sourceLabel} run failed {timeAgo(run.createdAt)}
|
<span>{timeAgo(run.createdAt)}</span>
|
||||||
</p>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
<div className="flex w-full flex-wrap items-center gap-2 sm:w-auto sm:justify-end">
|
</Link>
|
||||||
<Button
|
<div className="hidden shrink-0 items-center gap-2 sm:flex">
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
size="sm"
|
variant="outline"
|
||||||
className="h-8 shrink-0 px-2.5"
|
size="sm"
|
||||||
onClick={() => retryRun.mutate()}
|
className="h-8 shrink-0 px-2.5"
|
||||||
disabled={retryRun.isPending}
|
onClick={onRetry}
|
||||||
>
|
disabled={isRetrying}
|
||||||
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
>
|
||||||
{retryRun.isPending ? "Retrying…" : "Retry"}
|
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
||||||
</Button>
|
{isRetrying ? "Retrying…" : "Retry"}
|
||||||
<Button
|
</Button>
|
||||||
type="button"
|
<button
|
||||||
variant="outline"
|
type="button"
|
||||||
size="sm"
|
onClick={onDismiss}
|
||||||
className="h-8 shrink-0 px-2.5"
|
className="rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100"
|
||||||
asChild
|
aria-label="Dismiss"
|
||||||
>
|
>
|
||||||
<Link to={`/agents/${run.agentId}/runs/${run.id}`}>
|
<X className="h-4 w-4" />
|
||||||
Open run
|
</button>
|
||||||
<ArrowUpRight className="ml-1.5 h-3.5 w-3.5" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm">
|
<div className="mt-3 flex gap-2 sm:hidden">
|
||||||
{displayError}
|
<Button
|
||||||
</div>
|
type="button"
|
||||||
|
variant="outline"
|
||||||
<div className="text-xs">
|
size="sm"
|
||||||
<span className="font-mono text-muted-foreground">run {run.id.slice(0, 8)}</span>
|
className="h-8 shrink-0 px-2.5"
|
||||||
</div>
|
onClick={onRetry}
|
||||||
|
disabled={isRetrying}
|
||||||
{retryRun.isError && (
|
>
|
||||||
<div className="text-xs text-destructive">
|
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
||||||
{retryRun.error instanceof Error ? retryRun.error.message : "Failed to retry run"}
|
{isRetrying ? "Retrying…" : "Retry"}
|
||||||
</div>
|
</Button>
|
||||||
)}
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onDismiss}
|
||||||
|
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
|
aria-label="Dismiss"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -473,13 +426,19 @@ export function Inbox() {
|
||||||
const showFailedRunsCategory =
|
const showFailedRunsCategory =
|
||||||
allCategoryFilter === "everything" || allCategoryFilter === "failed_runs";
|
allCategoryFilter === "everything" || allCategoryFilter === "failed_runs";
|
||||||
const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts";
|
const showAlertsCategory = allCategoryFilter === "everything" || allCategoryFilter === "alerts";
|
||||||
|
const failedRunsForTab = useMemo(() => {
|
||||||
|
if (tab === "all" && !showFailedRunsCategory) return [];
|
||||||
|
return failedRuns;
|
||||||
|
}, [failedRuns, tab, showFailedRunsCategory]);
|
||||||
|
|
||||||
const workItemsToRender = useMemo(
|
const workItemsToRender = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getInboxWorkItems({
|
getInboxWorkItems({
|
||||||
issues: tab === "all" && !showTouchedCategory ? [] : issuesToRender,
|
issues: tab === "all" && !showTouchedCategory ? [] : issuesToRender,
|
||||||
approvals: tab === "all" && !showApprovalsCategory ? [] : approvalsToRender,
|
approvals: tab === "all" && !showApprovalsCategory ? [] : approvalsToRender,
|
||||||
|
failedRuns: failedRunsForTab,
|
||||||
}),
|
}),
|
||||||
[approvalsToRender, issuesToRender, showApprovalsCategory, showTouchedCategory, tab],
|
[approvalsToRender, issuesToRender, showApprovalsCategory, showTouchedCategory, tab, failedRunsForTab],
|
||||||
);
|
);
|
||||||
|
|
||||||
const agentName = (id: string | null) => {
|
const agentName = (id: string | null) => {
|
||||||
|
|
@ -538,6 +497,33 @@ export function Inbox() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const retryRunMutation = useMutation({
|
||||||
|
mutationFn: async (run: HeartbeatRun) => {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
const context = run.contextSnapshot as Record<string, unknown> | null;
|
||||||
|
if (context) {
|
||||||
|
if (typeof context.issueId === "string" && context.issueId) payload.issueId = context.issueId;
|
||||||
|
if (typeof context.taskId === "string" && context.taskId) payload.taskId = context.taskId;
|
||||||
|
if (typeof context.taskKey === "string" && context.taskKey) payload.taskKey = context.taskKey;
|
||||||
|
}
|
||||||
|
const result = await agentsApi.wakeup(run.agentId, {
|
||||||
|
source: "on_demand",
|
||||||
|
triggerDetail: "manual",
|
||||||
|
reason: "retry_failed_run",
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
if (!("id" in result)) {
|
||||||
|
throw new Error("Retry was skipped because the agent is not currently invokable.");
|
||||||
|
}
|
||||||
|
return { newRun: result, originalRun: run };
|
||||||
|
},
|
||||||
|
onSuccess: ({ newRun, originalRun }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(originalRun.companyId) });
|
||||||
|
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(originalRun.companyId, originalRun.agentId) });
|
||||||
|
navigate(`/agents/${originalRun.agentId}/runs/${newRun.id}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [fadingOutIssues, setFadingOutIssues] = useState<Set<string>>(new Set());
|
const [fadingOutIssues, setFadingOutIssues] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const invalidateInboxIssueQueries = () => {
|
const invalidateInboxIssueQueries = () => {
|
||||||
|
|
@ -607,13 +593,6 @@ export function Inbox() {
|
||||||
const showWorkItemsSection = workItemsToRender.length > 0;
|
const showWorkItemsSection = workItemsToRender.length > 0;
|
||||||
const showJoinRequestsSection =
|
const showJoinRequestsSection =
|
||||||
tab === "all" ? showJoinRequestsCategory && hasJoinRequests : tab === "unread" && hasJoinRequests;
|
tab === "all" ? showJoinRequestsCategory && hasJoinRequests : tab === "unread" && hasJoinRequests;
|
||||||
const showFailedRunsSection = shouldShowInboxSection({
|
|
||||||
tab,
|
|
||||||
hasItems: hasRunFailures,
|
|
||||||
showOnRecent: hasRunFailures,
|
|
||||||
showOnUnread: hasRunFailures,
|
|
||||||
showOnAll: showFailedRunsCategory && hasRunFailures,
|
|
||||||
});
|
|
||||||
const showAlertsSection = shouldShowInboxSection({
|
const showAlertsSection = shouldShowInboxSection({
|
||||||
tab,
|
tab,
|
||||||
hasItems: hasAlerts,
|
hasItems: hasAlerts,
|
||||||
|
|
@ -623,7 +602,6 @@ export function Inbox() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleSections = [
|
const visibleSections = [
|
||||||
showFailedRunsSection ? "failed_runs" : null,
|
|
||||||
showAlertsSection ? "alerts" : null,
|
showAlertsSection ? "alerts" : null,
|
||||||
showJoinRequestsSection ? "join_requests" : null,
|
showJoinRequestsSection ? "join_requests" : null,
|
||||||
showWorkItemsSection ? "work_items" : null,
|
showWorkItemsSection ? "work_items" : null,
|
||||||
|
|
@ -751,6 +729,21 @@ export function Inbox() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.kind === "failed_run") {
|
||||||
|
return (
|
||||||
|
<FailedRunInboxRow
|
||||||
|
key={`run:${item.run.id}`}
|
||||||
|
run={item.run}
|
||||||
|
issueById={issueById}
|
||||||
|
agentName={agentName(item.run.agentId)}
|
||||||
|
issueLinkState={issueLinkState}
|
||||||
|
onDismiss={() => dismiss(`run:${item.run.id}`)}
|
||||||
|
onRetry={() => retryRunMutation.mutate(item.run)}
|
||||||
|
isRetrying={retryRunMutation.isPending}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const issue = item.issue;
|
const issue = item.issue;
|
||||||
const isUnread = issue.isUnreadForMe && !fadingOutIssues.has(issue.id);
|
const isUnread = issue.isUnreadForMe && !fadingOutIssues.has(issue.id);
|
||||||
const isFading = fadingOutIssues.has(issue.id);
|
const isFading = fadingOutIssues.has(issue.id);
|
||||||
|
|
@ -857,28 +850,6 @@ export function Inbox() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showFailedRunsSection && (
|
|
||||||
<>
|
|
||||||
{showSeparatorBefore("failed_runs") && <Separator />}
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
||||||
Failed Runs
|
|
||||||
</h3>
|
|
||||||
<div className="grid gap-3">
|
|
||||||
{failedRuns.map((run) => (
|
|
||||||
<FailedRunCard
|
|
||||||
key={run.id}
|
|
||||||
run={run}
|
|
||||||
issueById={issueById}
|
|
||||||
agentName={agentName(run.agentId)}
|
|
||||||
issueLinkState={issueLinkState}
|
|
||||||
onDismiss={() => dismiss(`run:${run.id}`)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showAlertsSection && (
|
{showAlertsSection && (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue