106 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
}
|