feat(dashboard): run paired inbox actions (#34)

This commit is contained in:
Eyejoker
2026-04-27 02:39:41 +09:00
committed by GitHub
parent 1676f53f4d
commit 16aeb0449d
5 changed files with 493 additions and 0 deletions

View File

@@ -1,11 +1,13 @@
import { useEffect, useMemo, useState, type ReactNode } from 'react';
import {
type DashboardInboxAction,
type DashboardTaskAction,
type DashboardOverview,
type DashboardTask,
type StatusSnapshot,
fetchDashboardData,
runInboxAction,
runScheduledTaskAction,
sendRoomMessage,
} from './api';
@@ -36,6 +38,7 @@ type DashboardView = 'usage' | 'inbox' | 'health' | 'rooms' | 'scheduled';
type TaskGroupKey = 'watchers' | 'scheduled' | 'paused' | 'completed';
type TaskResultTone = 'ok' | 'fail' | 'none';
type TaskActionKey = `${string}:${DashboardTaskAction}`;
type InboxActionKey = `${string}:${DashboardInboxAction}`;
type InboxFilter = 'all' | InboxItem['kind'];
type HealthLevel = 'ok' | 'stale' | 'down';
@@ -292,6 +295,30 @@ function taskActionsFor(task: DashboardTask): DashboardTaskAction[] {
return [];
}
function inboxActionsFor(item: InboxItem): DashboardInboxAction[] {
if (item.source !== 'paired-task') return [];
if (
item.kind === 'reviewer-request' ||
item.kind === 'approval' ||
item.kind === 'arbiter-request'
) {
return ['run'];
}
return [];
}
function inboxActionLabel(
item: InboxItem,
action: DashboardInboxAction,
t: Messages,
): string {
if (action !== 'run') return action;
if (item.kind === 'reviewer-request') return t.inbox.actions.runReview;
if (item.kind === 'approval') return t.inbox.actions.finalize;
if (item.kind === 'arbiter-request') return t.inbox.actions.runArbiter;
return t.inbox.actions.run;
}
const INBOX_FILTERS: InboxFilter[] = [
'all',
'ci-failure',
@@ -600,14 +627,18 @@ function InboxPanel({
overview,
tasks,
locale,
onInboxAction,
onTaskAction,
inboxActionKey,
taskActionKey,
t,
}: {
overview: DashboardOverview;
tasks: DashboardTask[];
locale: Locale;
onInboxAction: (item: InboxItem, action: DashboardInboxAction) => void;
onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void;
inboxActionKey: InboxActionKey | null;
taskActionKey: TaskActionKey | null;
t: Messages;
}) {
@@ -690,6 +721,7 @@ function InboxPanel({
const linkedTaskActions = linkedTask
? taskActionsFor(linkedTask)
: [];
const inboxActions = inboxActionsFor(item);
return (
<article
className={`inbox-card inbox-${item.severity}`}
@@ -754,6 +786,27 @@ function InboxPanel({
})}
</div>
) : null}
{inboxActions.length > 0 ? (
<div className="task-actions inbox-actions">
{inboxActions.map((action) => {
const actionKey: InboxActionKey = `${item.id}:${action}`;
const busy = inboxActionKey === actionKey;
return (
<button
className={`task-action task-action-${action}`}
disabled={busy}
key={action}
onClick={() => onInboxAction(item, action)}
type="button"
>
{busy
? t.inbox.actions.busy
: inboxActionLabel(item, action, t)}
</button>
);
})}
</div>
) : null}
</article>
);
})}
@@ -1429,6 +1482,9 @@ function App() {
const [taskActionKey, setTaskActionKey] = useState<TaskActionKey | null>(
null,
);
const [inboxActionKey, setInboxActionKey] = useState<InboxActionKey | null>(
null,
);
const [roomMessageKey, setRoomMessageKey] = useState<string | null>(null);
const t = messages[locale];
@@ -1478,6 +1534,22 @@ function App() {
}
}
async function handleInboxAction(
item: InboxItem,
action: DashboardInboxAction,
) {
const actionKey: InboxActionKey = `${item.id}:${action}`;
setInboxActionKey(actionKey);
try {
await runInboxAction(item.id, action);
await refresh(false);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setInboxActionKey(null);
}
}
async function handleRoomMessage(
roomJid: string,
text: string,
@@ -1591,7 +1663,11 @@ function App() {
<span>{t.panels.inboxQueue}</span>
</div>
<InboxPanel
inboxActionKey={inboxActionKey}
locale={locale}
onInboxAction={(item, action) =>
void handleInboxAction(item, action)
}
onTaskAction={(task, action) =>
void handleTaskAction(task, action)
}

View File

@@ -101,6 +101,7 @@ export interface DashboardTask {
}
export type DashboardTaskAction = 'pause' | 'resume' | 'cancel';
export type DashboardInboxAction = 'run';
async function fetchJson<T>(path: string): Promise<T> {
const response = await fetch(path, {
@@ -162,6 +163,21 @@ export async function runScheduledTaskAction(
});
}
export async function runInboxAction(
inboxId: string,
action: DashboardInboxAction,
): Promise<{
ok: true;
id: string;
taskId: string;
intentKind: string;
queued: boolean;
}> {
return postJson(`/api/inbox/${encodeURIComponent(inboxId)}/actions`, {
action,
});
}
export async function sendRoomMessage(
roomJid: string,
text: string,

View File

@@ -104,6 +104,13 @@ export interface Messages {
warn: string;
error: string;
};
actions: {
run: string;
busy: string;
runReview: string;
finalize: string;
runArbiter: string;
};
};
rooms: {
empty: string;
@@ -324,6 +331,13 @@ export const messages = {
warn: '주의',
error: '위험',
},
actions: {
run: 'Run',
busy: 'Running...',
runReview: 'Run review',
finalize: 'Finalize',
runArbiter: 'Run arbiter',
},
},
rooms: {
empty: '룸 없음.',
@@ -528,6 +542,13 @@ export const messages = {
warn: 'Warn',
error: 'Risk',
},
actions: {
run: 'Run',
busy: 'Running...',
runReview: 'Run review',
finalize: 'Finalize',
runArbiter: 'Run arbiter',
},
},
rooms: {
empty: 'No rooms yet.',
@@ -732,6 +753,13 @@ export const messages = {
warn: '关注',
error: '风险',
},
actions: {
run: '运行',
busy: '运行中...',
runReview: '运行评审',
finalize: '完成',
runArbiter: '运行仲裁',
},
},
rooms: {
empty: '暂无房间。',
@@ -936,6 +964,13 @@ export const messages = {
warn: '注意',
error: '危険',
},
actions: {
run: '実行',
busy: '実行中...',
runReview: 'レビュー実行',
finalize: 'Finalize',
runArbiter: '仲裁実行',
},
},
rooms: {
empty: 'ルームなし。',