XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
import { useEffect, useState, type FormEvent } from 'react';
import { Beaker, CheckCircle2, FileJson2, FileLock2, Pencil, Rocket, Save, SearchCode, X } from 'lucide-react';
import { useApiResource, useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import { hasCapability } from '../lib/permissions';
import type { CommandPolicyDetail, CommandPolicySummary, PolicySimulation, PolicyValidation } from '../types';
import { Badge, Button, Field, Input, PageHeader, Panel, ResourceState, TableShell, Textarea, Toast, formatDate } from '../components/ui';
import { ModalPortal } from '../components/modal-portal';
import { normalizePolicyRule, PolicyRuleEditor } from '../components/policy-rule-editor';
import type { CommandPolicyRule } from '../types';

export function PoliciesPage() {
  const { t, locale } = useI18n();
  const { api, session } = useServer();
  const canPublish = hasCapability(session?.actor, 'policy_publish');
  const resource = useApiResource(() => api.policies(), [], ['policy-published']);
  const [command, setCommand] = useState('');
  const [playerUuid, setPlayerUuid] = useState('');
  const [simulation, setSimulation] = useState<PolicySimulation>();
  const [simulationError, setSimulationError] = useState<string>();
  const [simulating, setSimulating] = useState(false);
  const [publish, setPublish] = useState<CommandPolicySummary>();
  const [reason, setReason] = useState('');
  const [editorPolicy, setEditorPolicy] = useState<CommandPolicyDetail>();
  const commandCatalogResource = useApiResource(
    () => api.policyCommands(),
    [],
    ['triggers-changed'],
    Boolean(editorPolicy),
  );
  const [rules, setRules] = useState<CommandPolicyRule[]>([]);
  const [draftDescription, setDraftDescription] = useState('');
  const [editorBusy, setEditorBusy] = useState(false);
  const [validation, setValidation] = useState<PolicyValidation>();
  const [editorError, setEditorError] = useState<string>();
  const [toast, setToast] = useState<{ message: string; tone: 'good' | 'danger' }>();

  const simulate = async (event: FormEvent) => {
    event.preventDefault();
    if (!command.trim()) return;
    setSimulating(true);
    try {
      setSimulation(await api.simulatePolicy(command.trim(), playerUuid.trim() || undefined));
      setSimulationError(undefined);
    } catch (error) {
      setSimulation(undefined);
      setSimulationError(error instanceof Error ? error.message : String(error));
    } finally {
      setSimulating(false);
    }
  };

  const publishPolicy = async (event: FormEvent) => {
    event.preventDefault();
    if (!publish || !reason.trim()) return;
    try {
      const operation = await api.publishPolicy(publish.id, publish.version, reason.trim());
      setToast({ message: `${t('operation.queued')} · ${operation.id}`, tone: 'good' });
      setPublish(undefined);
      setReason('');
      resource.reload();
    } catch (error) {
      setToast({ message: error instanceof Error ? error.message : String(error), tone: 'danger' });
    }
  };

  const openEditor = async (policy: CommandPolicySummary) => {
    setEditorBusy(true);
    setEditorError(undefined);
    setValidation(undefined);
    try {
      const detail = await api.policy(policy.id);
      setEditorPolicy(detail);
      setRules(detail.rules.map(normalizePolicyRule));
      setDraftDescription(detail.description ?? detail.name);
      setValidation(detail.validation);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      setEditorError(message);
      setToast({ message, tone: 'danger' });
    } finally {
      setEditorBusy(false);
    }
  };

  const persistPolicy = async (publishImmediately: boolean) => {
    const active = resource.data?.items.find((item) => item.state === 'ACTIVE');
    if (!active || !draftDescription.trim()) return;
    setEditorBusy(true);
    setEditorError(undefined);
    try {
      const operation = await api.stagePolicy(
        active.version,
        draftDescription.trim(),
        rules,
      );
      if (publishImmediately) {
        if (!operation.policy) throw new Error('Server did not return the staged policy version.');
        const applied = await api.publishPolicy(
          operation.policy.id,
          operation.policy.version,
          draftDescription.trim(),
        );
        setToast({ message: `${t('policies.appliedImmediately')} · ${applied.id}`, tone: 'good' });
      } else {
        setToast({ message: `${t('policies.draftSaved')} · ${operation.id}`, tone: 'good' });
      }
      setEditorPolicy(undefined);
      setValidation(undefined);
      resource.reload();
    } catch (error) {
      setEditorError(error instanceof Error ? error.message : String(error));
    } finally {
      setEditorBusy(false);
    }
  };

  const saveDraft = (event: FormEvent) => {
    event.preventDefault();
    void persistPolicy(canPublish);
  };

  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's' || !editorPolicy) return;
      event.preventDefault();
      if (!editorBusy && draftDescription.trim()) void persistPolicy(canPublish);
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [canPublish, draftDescription, editorBusy, editorPolicy, persistPolicy]);

  const validateDraft = async (policy: CommandPolicySummary) => {
    setEditorBusy(true);
    setEditorError(undefined);
    try {
      const [result, detail] = await Promise.all([api.validatePolicy(policy.id), api.policy(policy.id)]);
      setEditorPolicy(detail);
      setRules(detail.rules.map(normalizePolicyRule));
      setDraftDescription(detail.description ?? detail.name);
      setValidation(result);
      setToast({ message: t('policies.validationComplete'), tone: result.valid ? 'good' : 'danger' });
    } catch (error) {
      setEditorError(error instanceof Error ? error.message : String(error));
    } finally {
      setEditorBusy(false);
    }
  };

  const validationLine = (entry: string | { ruleId?: string; code?: string; message: string }) =>
    typeof entry === 'string' ? entry : [entry.ruleId, entry.code, entry.message].filter(Boolean).join(' · ');

  return (
    <div className="page">
      <PageHeader title={t('policies.title')} description={t('policies.description')} />
      <div className="policy-layout">
        <Panel title={<><FileLock2 size={18} />{t('policies.title')}</>}>
          <ResourceState loading={resource.loading} error={resource.error} empty={resource.data?.items.length === 0} onRetry={resource.reload}>
            <TableShell>
              <thead><tr><th>{t('policies.name')}</th><th>{t('policies.version')}</th><th>{t('policies.rules')}</th><th>{t('policies.state')}</th><th>{t('policies.updated')}</th><th /></tr></thead>
              <tbody>{resource.data?.items.map((policy) => (
                <tr key={`${policy.id}:${policy.version}`}>
                  <td><strong>{policy.name}</strong>{policy.description && <small className="table-subline">{policy.description}</small>}</td>
                  <td><code>v{policy.version}</code></td><td>{policy.groupCount ?? (policy.ruleCount > 0 ? 1 : 0)} {t('policies.groups')} · {policy.ruleCount} {t('policies.rules')}</td>
                  <td><Badge tone={policy.state === 'ACTIVE' ? 'good' : policy.state === 'DRAFT' ? 'warn' : 'neutral'}>{policy.state}</Badge></td>
                  <td>{formatDate(policy.updatedAt, locale)}</td>
                  <td><div className="button-row"><Button variant="ghost" disabled={editorBusy} onClick={() => void openEditor(policy)}><Pencil size={14} />{t('policies.edit')}</Button>{policy.state === 'DRAFT' && <><Button variant="ghost" disabled={editorBusy} onClick={() => void validateDraft(policy)}><CheckCircle2 size={14} />{t('policies.validate')}</Button>{canPublish && <Button variant="secondary" onClick={() => setPublish(policy)}><Rocket size={14} />{t('policies.publish')}</Button>}</>}</div></td>
                </tr>
              ))}</tbody>
            </TableShell>
          </ResourceState>
        </Panel>
        <Panel title={<><Beaker size={18} />{t('policies.simulator')}</>} className="simulator-panel">
          <form className="form-stack" onSubmit={simulate}>
            <Field label={t('policies.command')}><Input value={command} onChange={(event) => setCommand(event.target.value)} placeholder="execute as @e[type=minecraft:zombie] run tp @s ~ ~1 ~" required spellCheck={false} /></Field>
            <Field label={t('policies.playerUuid')}><Input value={playerUuid} onChange={(event) => setPlayerUuid(event.target.value)} placeholder="00000000-0000-0000-0000-000000000000" /></Field>
            <Button type="submit" disabled={simulating || !command.trim()}><SearchCode size={16} />{t('policies.run')}</Button>
          </form>
          <div className="simulation-result">
            <span className="field__label">{t('policies.result')}</span>
            {simulation ? <><Badge tone={simulation.decision === 'DENY' ? 'danger' : simulation.decision === 'GRANT' ? 'good' : 'info'}>{simulation.decision}</Badge><code className="command-normalized">{simulation.normalizedCommand ?? command}</code><ol>{simulation.explanation.map((line, index) => <li key={`${index}:${line}`}>{line}</li>)}</ol></> : simulationError ? <p className="error-text">{simulationError}</p> : <p className="muted">{t('policies.noResult')}</p>}
          </div>
        </Panel>
      </div>
      {editorPolicy && <Panel title={<><FileJson2 size={18} />{t('policies.editor')}</>}>
        <form className="form-stack" onSubmit={saveDraft}>
          <div className="confirm-box"><FileLock2 /><div><p>{t('policies.editorHint')}</p><p>{t('policies.liveHint')}</p></div></div>
          <Field label={t('policies.draftDescription')}><Input value={draftDescription} onChange={(event) => setDraftDescription(event.target.value)} maxLength={1000} required /></Field>
          <PolicyRuleEditor rules={rules} onChange={(next) => { setRules(next); setValidation(undefined); }}
            commandCatalog={commandCatalogResource.data?.commands ?? []}
            commandCatalogLoading={commandCatalogResource.loading}
            commandCatalogError={commandCatalogResource.error}
            onReloadCommandCatalog={commandCatalogResource.reload} />
          {validation && <div className={`confirm-box ${validation.valid ? '' : 'confirm-box--danger'}`}><CheckCircle2 /><div><strong>{validation.valid ? t('policies.valid') : t('policies.invalid')}</strong>{[...validation.errors, ...validation.warnings].map((entry, index) => <p key={index}>{validationLine(entry)}</p>)}</div></div>}
          {editorError && <p className="error-text" role="alert">{editorError}</p>}
          <div className="button-row"><Button type="button" variant="ghost" onClick={() => { setEditorPolicy(undefined); setEditorError(undefined); }}>{t('common.cancel')}</Button>{canPublish && <Button type="button" variant="secondary" disabled={editorBusy || !draftDescription.trim()} onClick={() => void persistPolicy(false)}><Save size={15} />{t('policies.saveDraft')}</Button>}<Button type="submit" disabled={editorBusy || !draftDescription.trim()}>{canPublish ? <Rocket size={15} /> : <Save size={15} />}{canPublish ? t('policies.saveAndApply') : t('policies.saveDraft')}</Button><span className="keyboard-hint"><kbd>Ctrl</kbd>+<kbd>S</kbd></span></div>
        </form>
      </Panel>}
      {publish && <ModalPortal><div className="modal-backdrop"><section className="modal" role="dialog" aria-modal="true"><div className="modal__header"><div><span className="page-header__eyebrow">{publish.id} · v{publish.version}</span><h2>{t('policies.publish')}</h2></div><button onClick={() => setPublish(undefined)} aria-label={t('common.close')}><X /></button></div><form className="form-stack" onSubmit={publishPolicy}><div className="confirm-box"><CheckCircle2 /><p>{t('policies.publishWarning')}</p></div><Field label={t('common.reason')}><Textarea required value={reason} onChange={(event) => setReason(event.target.value)} /></Field><div className="modal__actions"><Button type="button" variant="ghost" onClick={() => setPublish(undefined)}>{t('common.cancel')}</Button><Button type="submit" disabled={!reason.trim()}><Rocket size={15} />{t('policies.publish')}</Button></div></form></section></div></ModalPortal>}
      {toast && <Toast {...toast} onClose={() => setToast(undefined)} />}
    </div>
  );
}