Show guarded apply blockers in IDE
This commit is contained in:
@@ -70,6 +70,7 @@ type AuthoringChange = ProjectWorkspaceData["authoringChanges"][number];
|
||||
type ProjectVersion = ProjectWorkspaceData["projectVersions"][number];
|
||||
type VersionDetail = Awaited<ReturnType<typeof getLineageVersions>>[number];
|
||||
type VersionDiff = Awaited<ReturnType<typeof getVersionDiff>>;
|
||||
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({
|
||||
<EditorToolbarButton icon={GitCompareArrows} label={t.semanticDiff} onClick={openSemanticDiff} />
|
||||
<EditorToolbarButton icon={Sparkles} label={t.quickFixes} onClick={openKnowledgePanel} />
|
||||
<EditorToolbarButton icon={FileText} label={t.documentationMode} onClick={openDocumentationPanel} />
|
||||
<EditorToolbarButton actionId="apply-to-sfera" disabled={!applyEnabled || isApplying || isRefreshingPreview || !canApplyPayload} icon={CheckCircle2} label={applyLabel} onClick={handleApplyToSfera} />
|
||||
<EditorToolbarButton actionId="apply-to-sfera" disabled={!applyEnabled || isGuardBlocked || isApplying || isRefreshingPreview || !canApplyPayload} icon={CheckCircle2} label={applyLabel} onClick={handleApplyToSfera} />
|
||||
<EditorToolbarButton icon={History} label={t.rollbackPlan} onClick={openSemanticDiff} />
|
||||
<Badge tone={canApplyPayload ? "success" : "warning"}>{canApplyPayload ? t.workspaceApplyReady : t.previewRequired}</Badge>
|
||||
{isGuardBlocked ? (
|
||||
<Badge tone="danger" title={firstBlockedGuardCheck?.message}>{language === "ru" ? `Блокировки: ${blockedGuardChecks.length}` : `Blocked: ${blockedGuardChecks.length}`}</Badge>
|
||||
) : (
|
||||
<Badge tone={canApplyPayload ? "success" : "warning"}>{canApplyPayload ? t.workspaceApplyReady : t.previewRequired}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
@@ -3961,6 +3975,73 @@ function formatGuardStatus(status: string, language: UiLanguage) {
|
||||
return status;
|
||||
}
|
||||
|
||||
function formatGuardName(name: string, language: UiLanguage) {
|
||||
const labels: Record<string, { en: string; ru: string }> = {
|
||||
"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 (
|
||||
<div className="space-y-2" data-guard-summary>
|
||||
{checks.length === 0 ? (
|
||||
emptyRows.map((row) => (
|
||||
<div className="truncate text-xs text-muted-foreground" key={row}>{row}</div>
|
||||
))
|
||||
) : (
|
||||
checks.map((check) => (
|
||||
<div className="grid gap-1 border-b border-border/60 pb-2 last:border-b-0 last:pb-0" data-guard-check data-guard-name={check.name} data-guard-status={check.status} key={`${check.name}:${check.status}:${check.message}`}>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Badge tone={getGuardTone(check.status)}>{formatGuardStatus(check.status, language)}</Badge>
|
||||
<span className="truncate text-xs font-medium">{formatGuardName(check.name, language)}</span>
|
||||
</div>
|
||||
<div className="text-xs leading-5 text-muted-foreground">{check.message}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function sourceLocationFromNode(node?: SirNode | null) {
|
||||
return {
|
||||
source_path: node?.source_path ?? null,
|
||||
@@ -4226,7 +4307,12 @@ function BottomWorkbenchPanel({
|
||||
</div>
|
||||
<div className="min-h-0 overflow-auto border-l border-border bg-muted/20">
|
||||
<BottomList title={t.outputPanel} rows={outputRows} />
|
||||
<BottomList title={t.guardedApply} rows={checks.length > 0 ? checks.map((check) => `${formatGuardStatus(check.status, language)}: ${check.message}`) : [t.previewRequired, t.impactBeforeApply, t.reviewBeforeApply]} />
|
||||
<div className="border-b border-border p-3">
|
||||
<div className="text-xs font-semibold uppercase text-muted-foreground">{t.guardedApply}</div>
|
||||
<div className="mt-2">
|
||||
<GuardChecklist checks={checks} emptyRows={[t.previewRequired, t.impactBeforeApply, t.reviewBeforeApply]} language={language} />
|
||||
</div>
|
||||
</div>
|
||||
<BottomList title={t.aiShort} rows={aiRows} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<Card className="min-h-0 rounded-none border-0 border-b border-border p-0 shadow-none">
|
||||
@@ -4298,12 +4386,12 @@ function DiffPanel({
|
||||
<div className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
||||
<span>{semanticPreview.version_preview.next_version_id}</span>
|
||||
<span>{t.affectedNodes}: {semanticPreview.affected_nodes.length}</span>
|
||||
<span>{semanticPreview.version_preview.apply_available ? t.guardedApply : t.workspaceApplyReady}</span>
|
||||
<span>{isApplyBlocked ? `${t.blocked}: ${blockedChecks.length}` : semanticPreview.version_preview.apply_available ? t.guardedApply : t.workspaceApplyReady}</span>
|
||||
<span>{t.productionApplyDisabled}</span>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
<ActionButton label={t.previewRequired} />
|
||||
<ActionButton disabled={!semanticPreview.version_preview.apply_available} label={t.applyToSfera} />
|
||||
<ActionButton disabled={!semanticPreview.version_preview.apply_available || isApplyBlocked} label={t.applyToSfera} />
|
||||
<ActionButton label={t.rollbackPlan} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -4318,18 +4406,8 @@ function DiffPanel({
|
||||
))}
|
||||
<div className="rounded-lg border border-warning/30 bg-warning/10 p-3 text-sm">
|
||||
<div className="font-medium text-warning">{t.guardedApply}</div>
|
||||
<div className="mt-2 grid gap-2 text-xs text-muted-foreground">
|
||||
{checks.length === 0 ? (
|
||||
<>
|
||||
<span>{t.previewRequired}</span>
|
||||
<span>{t.impactBeforeApply}</span>
|
||||
<span>{t.reviewBeforeApply}</span>
|
||||
</>
|
||||
) : (
|
||||
checks.map((check) => (
|
||||
<span key={check.name}>{formatGuardStatus(check.status, language)}: {check.message}</span>
|
||||
))
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<GuardChecklist checks={checks} emptyRows={[t.previewRequired, t.impactBeforeApply, t.reviewBeforeApply]} language={language} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user