maestro/ui/src/components/userfolder/BrowserSessionsPanel.tsx
oss-sync caa0d03900
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (fd190dc)
2026-06-06 00:39:26 +00:00

106 lines
5.4 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import {
listBrowserSessionProfiles, deleteBrowserSessionProfile, testBrowserSessionProfile,
type BrowserSessionProfile,
} from '../../api';
import { AddBrowserSessionDialog } from './AddBrowserSessionDialog';
function StatusPill({ status }: { status: BrowserSessionProfile['status'] }) {
const map: Record<BrowserSessionProfile['status'], string> = {
pending: 'bg-slate-200 text-slate-700',
active: 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
expired: 'bg-amber-100 dark:bg-amber-500/15 text-amber-800 dark:text-amber-300',
revoked: 'bg-slate-200 text-slate-500',
error: 'bg-rose-100 dark:bg-rose-500/15 text-rose-700 dark:text-rose-300',
};
const labels: Record<BrowserSessionProfile['status'], string> = {
pending: '保留中',
active: '有効',
expired: '期限切れ',
revoked: '無効化',
error: 'エラー',
};
return <span className={`inline-flex items-center rounded px-2 py-0.5 text-2xs font-medium ${map[status]}`}>{labels[status]}</span>;
}
export function BrowserSessionsPanel() {
const qc = useQueryClient();
const { data: profiles = [], isLoading } = useQuery({
queryKey: ['browser-session-profiles'],
queryFn: listBrowserSessionProfiles,
});
const del = useMutation({
mutationFn: (id: number) => deleteBrowserSessionProfile(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ['browser-session-profiles'] }),
});
const test = useMutation({
mutationFn: (id: number) => testBrowserSessionProfile(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ['browser-session-profiles'] }),
});
const [adding, setAdding] = useState(false);
const [reLoginProfileId, setReLoginProfileId] = useState<number | null>(null);
return (
<div className="h-full overflow-y-auto p-6">
<div className="max-w-2xl space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-slate-800"></h2>
<button onClick={() => { setReLoginProfileId(null); setAdding(true); }}
className="rounded-md bg-accent px-3 py-1.5 text-xs font-medium text-accent-fg hover:bg-accent-deep">
</button>
</div>
<p className="text-xs text-slate-500">
cookie / storageState {' '}
<code className="font-mono text-2xs bg-slate-100 px-1 py-0.5 rounded">session_profile_id</code>{' '}
</p>
{isLoading && <div className="text-xs text-slate-500"></div>}
<div className="rounded-md border border-hairline divide-y divide-hairline">
{profiles.length === 0 && !isLoading && (
<div className="px-3 py-6 text-center text-xs text-slate-400">
<div></div>
<div className="mt-1 text-slate-400"></div>
</div>
)}
{profiles.map(p => (
<div key={p.id} className="flex items-center justify-between px-3 py-2">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-[13px] font-medium text-slate-800 truncate">{p.label}</span>
<StatusPill status={p.status} />
<span className="text-[10px] font-mono text-slate-400">id={p.id}</span>
</div>
<div className="text-2xs text-slate-500 truncate">{p.startUrl}</div>
{p.lastError && <div className="text-2xs text-rose-600 truncate">{p.lastError}</div>}
<div className="text-2xs text-slate-400">
{p.lastSavedAt ? `保存: ${new Date(p.lastSavedAt).toLocaleString('ja-JP')}` : '未保存'}
{p.lastUsedAt && ` · 最終使用: ${new Date(p.lastUsedAt).toLocaleString('ja-JP')}`}
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={() => test.mutate(p.id)} disabled={test.isPending}
className="text-xs text-slate-700 hover:text-slate-900 px-2 py-1 rounded hover:bg-surface disabled:opacity-50"></button>
<button onClick={() => { setReLoginProfileId(p.id); setAdding(true); }}
className="text-xs text-slate-700 hover:text-slate-900 px-2 py-1 rounded hover:bg-surface"></button>
<button onClick={() => { if (confirm(`${p.label} を削除しますか?`)) del.mutate(p.id); }}
className="text-xs text-rose-600 hover:text-rose-800 dark:hover:text-rose-300 px-2 py-1 rounded hover:bg-rose-50 dark:hover:bg-rose-500/15"></button>
</div>
</div>
))}
</div>
{adding && (
<AddBrowserSessionDialog
existingProfile={reLoginProfileId ? profiles.find(p => p.id === reLoginProfileId) ?? null : null}
onClose={() => { setAdding(false); setReLoginProfileId(null); }}
/>
)}
</div>
</div>
);
}