feat(dashboard): run paired inbox actions (#34)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: 'ルームなし。',
|
||||
|
||||
Reference in New Issue
Block a user