This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
* ときだけ管理コントロールを出す。判定できない場合でも 403 はトーストで処理。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
listBrowserSessionProfiles, deleteBrowserSessionProfile, testBrowserSessionProfile,
|
||||
@@ -28,14 +29,6 @@ import { useAuthState } from '../../App';
|
||||
|
||||
type ShowToast = (message: string, variant?: 'success' | 'error') => void;
|
||||
|
||||
const STATUS_LABEL: Record<BrowserSessionProfile['status'], string> = {
|
||||
pending: '保存待ち',
|
||||
active: '有効',
|
||||
expired: '期限切れ',
|
||||
revoked: '失効',
|
||||
error: 'エラー',
|
||||
};
|
||||
|
||||
const STATUS_CLASS: 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',
|
||||
@@ -45,9 +38,10 @@ const STATUS_CLASS: Record<BrowserSessionProfile['status'], string> = {
|
||||
};
|
||||
|
||||
function StatusPill({ status }: { status: BrowserSessionProfile['status'] }) {
|
||||
const { t } = useTranslation('spaces');
|
||||
return (
|
||||
<span className={`inline-flex items-center rounded px-2 py-0.5 text-2xs font-medium ${STATUS_CLASS[status]}`}>
|
||||
{STATUS_LABEL[status]}
|
||||
{t(`browser.status.${status}`)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -57,6 +51,7 @@ function errMsg(e: unknown): string {
|
||||
}
|
||||
|
||||
export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; showToast?: ShowToast }) {
|
||||
const { t } = useTranslation('spaces');
|
||||
const auth = useAuthState();
|
||||
const qc = useQueryClient();
|
||||
|
||||
@@ -81,12 +76,12 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
const delSess = useMutation({
|
||||
mutationFn: (id: number) => deleteBrowserSessionProfile(id, spaceId),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['space-browser-sessions', spaceId] }),
|
||||
onError: (e) => showToast?.(`セッションの削除に失敗しました: ${errMsg(e)}`, 'error'),
|
||||
onError: (e) => showToast?.(t('browser.toast.sessionDeleteFailed', { msg: errMsg(e) }), 'error'),
|
||||
});
|
||||
const testSess = useMutation({
|
||||
mutationFn: (id: number) => testBrowserSessionProfile(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['space-browser-sessions', spaceId] }),
|
||||
onError: (e) => showToast?.(`セッションの検証に失敗しました: ${errMsg(e)}`, 'error'),
|
||||
onError: (e) => showToast?.(t('browser.toast.sessionTestFailed', { msg: errMsg(e) }), 'error'),
|
||||
});
|
||||
const [adding, setAdding] = useState(false);
|
||||
|
||||
@@ -102,12 +97,12 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
const delMacro = useMutation({
|
||||
mutationFn: (name: string) => deleteFolderFile('browser-macros', name, spaceId),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['space-browser-macros', spaceId] }),
|
||||
onError: (e) => showToast?.(`削除に失敗しました: ${errMsg(e)}`, 'error'),
|
||||
onError: (e) => showToast?.(t('browser.toast.deleteFailed', { msg: errMsg(e) }), 'error'),
|
||||
});
|
||||
const delRecording = useMutation({
|
||||
mutationFn: (name: string) => deleteFolderFile('recordings', name, spaceId),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['space-browser-recordings', spaceId] }),
|
||||
onError: (e) => showToast?.(`削除に失敗しました: ${errMsg(e)}`, 'error'),
|
||||
onError: (e) => showToast?.(t('browser.toast.deleteFailed', { msg: errMsg(e) }), 'error'),
|
||||
});
|
||||
|
||||
// ファイル内容プレビュー(マクロ・録画共通)。
|
||||
@@ -117,7 +112,7 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
const content = await getFolderFile(subdir, name, spaceId);
|
||||
setPreview({ name, content });
|
||||
} catch (e) {
|
||||
showToast?.(`内容の取得に失敗しました: ${errMsg(e)}`, 'error');
|
||||
showToast?.(t('browser.toast.contentFetchFailed', { msg: errMsg(e) }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +122,7 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
{/* ── セッション ── */}
|
||||
<section data-testid="space-browser-sessions">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h2 className="text-base font-semibold text-slate-800">ブラウザセッション</h2>
|
||||
<h2 className="text-base font-semibold text-slate-800">{t('browser.sessions.heading')}</h2>
|
||||
{canManage && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -135,22 +130,21 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
onClick={() => setAdding(true)}
|
||||
className="rounded-md bg-accent px-3 py-1.5 text-xs font-medium text-accent-fg hover:bg-accent-deep"
|
||||
>
|
||||
セッションを追加
|
||||
{t('browser.sessions.add')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="mb-3 text-xs text-slate-500">
|
||||
このワークスペースで共有するログイン済みブラウザセッション。各セッションは
|
||||
作成者の鍵で暗号化されるため、利用できるのは作成した本人だけです。
|
||||
{t('browser.sessions.intro')}
|
||||
</p>
|
||||
|
||||
{sessLoading && <div className="text-xs text-slate-500">読み込み中…</div>}
|
||||
{sessLoading && <div className="text-xs text-slate-500">{t('common:loading')}</div>}
|
||||
|
||||
<div className="divide-y divide-hairline rounded-md border border-hairline">
|
||||
{profiles.length === 0 && !sessLoading && (
|
||||
<div className="px-3 py-6 text-center text-xs text-slate-400">
|
||||
<div>このワークスペースにはまだブラウザセッションがありません。</div>
|
||||
{canManage && <div className="mt-1">「セッションを追加」からログインして保存してください。</div>}
|
||||
<div>{t('browser.sessions.empty')}</div>
|
||||
{canManage && <div className="mt-1">{t('browser.sessions.emptyHint')}</div>}
|
||||
</div>
|
||||
)}
|
||||
{profiles.map(p => {
|
||||
@@ -163,7 +157,7 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
<StatusPill status={p.status} />
|
||||
{!usable && (
|
||||
<span className="inline-flex items-center rounded bg-slate-200 px-2 py-0.5 text-2xs font-medium text-slate-600">
|
||||
作成者のみ利用可
|
||||
{t('browser.sessions.creatorOnly')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -171,7 +165,7 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
{p.lastError && <div className="truncate text-2xs text-rose-600">{p.lastError}</div>}
|
||||
{!usable && (
|
||||
<div className="text-2xs text-slate-400">
|
||||
このセッションは作成者の鍵で暗号化されています。閲覧はできますが、別のメンバーは復号・利用できません。
|
||||
{t('browser.sessions.creatorOnlyHint')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -183,16 +177,16 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
disabled={testSess.isPending}
|
||||
className="rounded px-2 py-1 text-xs text-slate-700 hover:bg-surface hover:text-slate-900 disabled:opacity-50"
|
||||
>
|
||||
検証
|
||||
{t('browser.sessions.test')}
|
||||
</button>
|
||||
)}
|
||||
{canManage && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { if (confirm(`「${p.label}」を削除しますか?`)) delSess.mutate(p.id); }}
|
||||
onClick={() => { if (confirm(t('browser.deleteConfirm', { name: p.label }))) delSess.mutate(p.id); }}
|
||||
className="rounded px-2 py-1 text-xs text-rose-600 hover:bg-rose-50 hover:text-rose-800 dark:hover:bg-rose-500/15 dark:hover:text-rose-300"
|
||||
>
|
||||
削除
|
||||
{t('common:delete')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -204,28 +198,28 @@ export function SpaceBrowserPanel({ spaceId, showToast }: { spaceId: string; sho
|
||||
|
||||
{/* ── マクロ ── */}
|
||||
<FolderSection
|
||||
title="ブラウザマクロ"
|
||||
title={t('browser.macros.title')}
|
||||
testid="space-browser-macros"
|
||||
subdir="browser-macros"
|
||||
query={macros}
|
||||
emptyText="このワークスペースにはまだブラウザマクロがありません。"
|
||||
hint="エージェントがブラウザ操作を記録すると、このワークスペースのマクロとして保存されます。"
|
||||
emptyText={t('browser.macros.empty')}
|
||||
hint={t('browser.macros.hint')}
|
||||
canManage={canManage}
|
||||
onView={(name) => openPreview('browser-macros', name)}
|
||||
onDelete={(name) => { if (confirm(`「${name}」を削除しますか?`)) delMacro.mutate(name); }}
|
||||
onDelete={(name) => { if (confirm(t('browser.deleteConfirm', { name }))) delMacro.mutate(name); }}
|
||||
/>
|
||||
|
||||
{/* ── 録画 ── */}
|
||||
<FolderSection
|
||||
title="録画"
|
||||
title={t('browser.recordings.title')}
|
||||
testid="space-browser-recordings"
|
||||
subdir="recordings"
|
||||
query={recordings}
|
||||
emptyText="このワークスペースにはまだ録画がありません。"
|
||||
hint="ブラウザ操作の記録(録画)がこのワークスペースのフォルダに保存されます。"
|
||||
emptyText={t('browser.recordings.empty')}
|
||||
hint={t('browser.recordings.hint')}
|
||||
canManage={canManage}
|
||||
onView={(name) => openPreview('recordings', name)}
|
||||
onDelete={(name) => { if (confirm(`「${name}」を削除しますか?`)) delRecording.mutate(name); }}
|
||||
onDelete={(name) => { if (confirm(t('browser.deleteConfirm', { name }))) delRecording.mutate(name); }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -253,12 +247,13 @@ interface FolderSectionProps {
|
||||
}
|
||||
|
||||
function FolderSection({ title, testid, query, emptyText, hint, canManage, onView, onDelete }: FolderSectionProps) {
|
||||
const { t } = useTranslation('spaces');
|
||||
const files = query.data ?? [];
|
||||
return (
|
||||
<section data-testid={testid}>
|
||||
<h2 className="mb-2 text-base font-semibold text-slate-800">{title}</h2>
|
||||
<p className="mb-3 text-xs text-slate-500">{hint}</p>
|
||||
{query.isLoading && <div className="text-xs text-slate-500">読み込み中…</div>}
|
||||
{query.isLoading && <div className="text-xs text-slate-500">{t('common:loading')}</div>}
|
||||
<div className="divide-y divide-hairline rounded-md border border-hairline">
|
||||
{files.length === 0 && !query.isLoading && (
|
||||
<div className="px-3 py-6 text-center text-xs text-slate-400">{emptyText}</div>
|
||||
@@ -279,7 +274,7 @@ function FolderSection({ title, testid, query, emptyText, hint, canManage, onVie
|
||||
onClick={() => onDelete(f.name)}
|
||||
className="ml-2 shrink-0 rounded px-2 py-1 text-xs text-rose-600 hover:bg-rose-50 hover:text-rose-800 dark:hover:bg-rose-500/15 dark:hover:text-rose-300"
|
||||
>
|
||||
削除
|
||||
{t('common:delete')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user