From 5b26c679470e8f36cab7a7dd1aefa9c8dce7d974 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 16 May 2026 20:11:23 +0300 Subject: [PATCH] Show guarded apply blockers in IDE --- .../src/components/editor/ide-workspace.tsx | 112 +++++++++++++++--- 1 file changed, 95 insertions(+), 17 deletions(-) diff --git a/frontend/sfera-web/src/components/editor/ide-workspace.tsx b/frontend/sfera-web/src/components/editor/ide-workspace.tsx index 878c899..c09b2a6 100644 --- a/frontend/sfera-web/src/components/editor/ide-workspace.tsx +++ b/frontend/sfera-web/src/components/editor/ide-workspace.tsx @@ -70,6 +70,7 @@ type AuthoringChange = ProjectWorkspaceData["authoringChanges"][number]; type ProjectVersion = ProjectWorkspaceData["projectVersions"][number]; type VersionDetail = Awaited>[number]; type VersionDiff = Awaited>; +type GuardCheck = { name: string; status: string; message: string }; type WorkspaceMode = "overview" | "module" | "form" | "properties" | "events" | "versions" | "documentation" | "knowledge" | "learning" | "flowchart"; type OpenDocument = { id: string; @@ -1985,6 +1986,10 @@ function EditorPanel({ const isRealSourceLoaded = Boolean(data.editorSourceText && !data.editorSourceText.startsWith("// Код модуля не загружен.")); const hasEditorChanges = normalizeEditorText(proposedText) !== normalizeEditorText(sourceText); const applyEnabled = semanticDiffPreview?.version_preview?.apply_available ?? false; + const activeGuardChecks = semanticDiffPreview?.checks ?? data.authoringPreview?.checks ?? []; + const blockedGuardChecks = getBlockingGuardChecks(activeGuardChecks); + const firstBlockedGuardCheck = blockedGuardChecks[0] ?? null; + const isGuardBlocked = blockedGuardChecks.length > 0; const canApplyPayload = Boolean(data.editorSelectedObject && data.editorSourceText && proposedText && hasEditorChanges); const navigationTarget = data.authoringPreview?.context.routine ?? data.authoringPreview?.context.object ?? selectedObject ?? null; const initialSymbolQuery = data.authoringPreview?.context.routine?.name ?? selectedMetadataNode?.label ?? selectedObject?.name ?? "Проведение"; @@ -2153,6 +2158,11 @@ function EditorPanel({ async function handleApplyToSfera() { const versionPreview = semanticDiffPreview?.version_preview; + if (isGuardBlocked) { + setApplyState("error"); + setApplyMessage(firstBlockedGuardCheck?.message ?? t.blocked); + return; + } if (!versionPreview?.apply_available || !canApplyPayload) { setApplyState("error"); setApplyMessage(t.previewRequired); @@ -2227,9 +2237,13 @@ function EditorPanel({ - + - {canApplyPayload ? t.workspaceApplyReady : t.previewRequired} + {isGuardBlocked ? ( + {language === "ru" ? `Блокировки: ${blockedGuardChecks.length}` : `Blocked: ${blockedGuardChecks.length}`} + ) : ( + {canApplyPayload ? t.workspaceApplyReady : t.previewRequired} + )}
@@ -3961,6 +3975,73 @@ function formatGuardStatus(status: string, language: UiLanguage) { return status; } +function formatGuardName(name: string, language: UiLanguage) { + const labels: Record = { + "ai-token-budget": { en: "AI token budget", ru: "Бюджет AI-токенов" }, + apply: { en: "Apply", ru: "Применение" }, + impact: { en: "Impact", ru: "Влияние" }, + preview: { en: "Preview", ru: "Предпросмотр" }, + privacy: { en: "Privacy", ru: "Приватность" }, + "production-1c": { en: "Production 1C", ru: "Продуктивная 1С" }, + rbac: { en: "RBAC", ru: "Права доступа" }, + review: { en: "Review", ru: "Ревью" }, + "task-session": { en: "Task session", ru: "Задача и сессия" }, + "workspace-history": { en: "Workspace history", ru: "История workspace" } + }; + const label = labels[name]; + if (label) { + return label[language]; + } + return name.replaceAll("-", " "); +} + +function getGuardTone(status: string): "neutral" | "success" | "warning" | "danger" | "info" { + if (status === "BLOCKED") { + return "danger"; + } + if (status === "READY" || status === "OK") { + return "success"; + } + if (status === "REQUIRED") { + return "warning"; + } + return "info"; +} + +function getBlockingGuardChecks(checks: GuardCheck[]) { + return checks.filter((check) => check.status === "BLOCKED"); +} + +function GuardChecklist({ + checks, + emptyRows, + language +}: Readonly<{ + checks: GuardCheck[]; + emptyRows: string[]; + language: UiLanguage; +}>) { + return ( +
+ {checks.length === 0 ? ( + emptyRows.map((row) => ( +
{row}
+ )) + ) : ( + checks.map((check) => ( +
+
+ {formatGuardStatus(check.status, language)} + {formatGuardName(check.name, language)} +
+
{check.message}
+
+ )) + )} +
+ ); +} + function sourceLocationFromNode(node?: SirNode | null) { return { source_path: node?.source_path ?? null, @@ -4226,7 +4307,12 @@ function BottomWorkbenchPanel({
- 0 ? checks.map((check) => `${formatGuardStatus(check.status, language)}: ${check.message}`) : [t.previewRequired, t.impactBeforeApply, t.reviewBeforeApply]} /> +
+
{t.guardedApply}
+
+ +
+
@@ -4287,6 +4373,8 @@ function DiffPanel({ data.authoringPreview?.semantic_diff ?? suggestedCode.map((text) => ({ kind: "ADD", text })); const checks = semanticPreview?.checks ?? data.authoringPreview?.checks ?? []; + const blockedChecks = getBlockingGuardChecks(checks); + const isApplyBlocked = blockedChecks.length > 0; return ( @@ -4298,12 +4386,12 @@ function DiffPanel({
{semanticPreview.version_preview.next_version_id} {t.affectedNodes}: {semanticPreview.affected_nodes.length} - {semanticPreview.version_preview.apply_available ? t.guardedApply : t.workspaceApplyReady} + {isApplyBlocked ? `${t.blocked}: ${blockedChecks.length}` : semanticPreview.version_preview.apply_available ? t.guardedApply : t.workspaceApplyReady} {t.productionApplyDisabled}
- +
@@ -4318,18 +4406,8 @@ function DiffPanel({ ))}
{t.guardedApply}
-
- {checks.length === 0 ? ( - <> - {t.previewRequired} - {t.impactBeforeApply} - {t.reviewBeforeApply} - - ) : ( - checks.map((check) => ( - {formatGuardStatus(check.status, language)}: {check.message} - )) - )} +
+