import { useCallback, useEffect, useState, type ReactNode } from 'react'; import { buildPageDataExportUrl, closePageDataDataset, getPageDataOpsOverview, resetPageDataPassword, restorePageDataRow, revokePageDataTokens, savePageDataPolicy, } from '../api/client'; import type { MindSpacePublishCheck, PageDataOpsOverview } from '../types'; type RowPolicyScope = 'all_rows' | 'own_rows' | 'owner_only'; function formatVisitorsText(visitors: Record | undefined) { return Object.entries(visitors ?? {}) .map(([userId, role]) => `${userId} ${role}`) .join('\n'); } function parseVisitorsText(text: string) { const visitors: Record = {}; for (const line of text.split('\n')) { const trimmed = line.trim(); if (!trimmed) continue; const match = trimmed.match(/^(\S+)[\s:]+(viewer|editor)$/i); if (!match) continue; visitors[match[1]] = match[2].toLowerCase() as 'viewer' | 'editor'; } return visitors; } type Props = { pageId: string; pageTitle: string; accessMode: MindSpacePublishCheck['accessMode']; onClose: () => void; }; function formatLogTime(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(); } export function MindSpacePageDataOpsPanel({ pageId, pageTitle, accessMode, onClose }: Props) { const [overview, setOverview] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [busyAction, setBusyAction] = useState(null); const [newPassword, setNewPassword] = useState(''); const [message, setMessage] = useState(null); const [defaultVisitorRole, setDefaultVisitorRole] = useState<'deny' | 'viewer' | 'editor'>('deny'); const [visitorsText, setVisitorsText] = useState(''); const [rowPolicies, setRowPolicies] = useState< Record >({}); const syncRoleForm = useCallback((policy: PageDataOpsOverview['policy']) => { setDefaultVisitorRole(policy.defaultVisitorRole ?? 'deny'); setVisitorsText(formatVisitorsText(policy.visitors)); const next: Record = {}; for (const [name, dataset] of Object.entries(policy.datasets ?? {})) { next[name] = { scope: dataset.rowPolicy?.scope ?? 'all_rows', ownerColumn: dataset.rowPolicy?.ownerColumn ?? 'created_by_user_id', }; } setRowPolicies(next); }, []); const loadOverview = useCallback(async () => { setLoading(true); setError(null); try { const next = await getPageDataOpsOverview(pageId); setOverview(next); syncRoleForm(next.policy); } catch (err) { const text = err instanceof Error ? err.message : '加载页面数据运维信息失败'; if (text.includes('policy_not_found') || text.includes('策略未配置')) { setOverview(null); setError('当前页面尚未配置 Page Data 策略。请让 Agent 注册 dataset 并调用 private_data_set_page_policy。'); } else { setError(text); } } finally { setLoading(false); } }, [pageId, syncRoleForm]); useEffect(() => { void loadOverview(); }, [loadOverview]); const runAction = async (key: string, fn: () => Promise) => { setBusyAction(key); setMessage(null); try { await fn(); await loadOverview(); } catch (err) { setMessage(err instanceof Error ? err.message : '操作失败'); } finally { setBusyAction(null); } }; const recentDeletions = overview?.recentLogs.filter( (entry) => entry.action === 'soft_delete' && entry.dataset && entry.rowId != null, ) ?? []; return (

PAGE DATA

页面数据运维

{pageTitle} · 页面 ID {pageId}
{loading ?

加载中…

: null} {error ?

{error}

: null} {message ?

{message}

: null} {overview ? ( <>
访问模式 {overview.publication.accessMode}
策略索引 {overview.index?.scopeHash ?? '—'}
活跃会话 {overview.activeSessionCount}
Dataset 数 {overview.datasets.length}
最近提交 {overview.recentSubmissions.length}
{overview.unconfiguredDatasets && overview.unconfiguredDatasets.length > 0 ? (

已注册但未纳入策略的 Dataset

{overview.unconfiguredDatasets.map((name) => ( {name} )).reduce((acc, node, index) => ( index === 0 ? [node] : [...acc, '、', node] ), [])} {' '}— 需通过 Agent 更新 Page Data 策略后才能公开访问。

) : null} {overview.publication.accessMode === 'login_required' ? (

登录访问 · 角色与行级策略