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 ProjectVersion = ProjectWorkspaceData["projectVersions"][number];
|
||||||
type VersionDetail = Awaited<ReturnType<typeof getLineageVersions>>[number];
|
type VersionDetail = Awaited<ReturnType<typeof getLineageVersions>>[number];
|
||||||
type VersionDiff = Awaited<ReturnType<typeof getVersionDiff>>;
|
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 WorkspaceMode = "overview" | "module" | "form" | "properties" | "events" | "versions" | "documentation" | "knowledge" | "learning" | "flowchart";
|
||||||
type OpenDocument = {
|
type OpenDocument = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1985,6 +1986,10 @@ function EditorPanel({
|
|||||||
const isRealSourceLoaded = Boolean(data.editorSourceText && !data.editorSourceText.startsWith("// Код модуля не загружен."));
|
const isRealSourceLoaded = Boolean(data.editorSourceText && !data.editorSourceText.startsWith("// Код модуля не загружен."));
|
||||||
const hasEditorChanges = normalizeEditorText(proposedText) !== normalizeEditorText(sourceText);
|
const hasEditorChanges = normalizeEditorText(proposedText) !== normalizeEditorText(sourceText);
|
||||||
const applyEnabled = semanticDiffPreview?.version_preview?.apply_available ?? false;
|
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 canApplyPayload = Boolean(data.editorSelectedObject && data.editorSourceText && proposedText && hasEditorChanges);
|
||||||
const navigationTarget = data.authoringPreview?.context.routine ?? data.authoringPreview?.context.object ?? selectedObject ?? null;
|
const navigationTarget = data.authoringPreview?.context.routine ?? data.authoringPreview?.context.object ?? selectedObject ?? null;
|
||||||
const initialSymbolQuery = data.authoringPreview?.context.routine?.name ?? selectedMetadataNode?.label ?? selectedObject?.name ?? "Проведение";
|
const initialSymbolQuery = data.authoringPreview?.context.routine?.name ?? selectedMetadataNode?.label ?? selectedObject?.name ?? "Проведение";
|
||||||
@@ -2153,6 +2158,11 @@ function EditorPanel({
|
|||||||
|
|
||||||
async function handleApplyToSfera() {
|
async function handleApplyToSfera() {
|
||||||
const versionPreview = semanticDiffPreview?.version_preview;
|
const versionPreview = semanticDiffPreview?.version_preview;
|
||||||
|
if (isGuardBlocked) {
|
||||||
|
setApplyState("error");
|
||||||
|
setApplyMessage(firstBlockedGuardCheck?.message ?? t.blocked);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!versionPreview?.apply_available || !canApplyPayload) {
|
if (!versionPreview?.apply_available || !canApplyPayload) {
|
||||||
setApplyState("error");
|
setApplyState("error");
|
||||||
setApplyMessage(t.previewRequired);
|
setApplyMessage(t.previewRequired);
|
||||||
@@ -2227,9 +2237,13 @@ function EditorPanel({
|
|||||||
<EditorToolbarButton icon={GitCompareArrows} label={t.semanticDiff} onClick={openSemanticDiff} />
|
<EditorToolbarButton icon={GitCompareArrows} label={t.semanticDiff} onClick={openSemanticDiff} />
|
||||||
<EditorToolbarButton icon={Sparkles} label={t.quickFixes} onClick={openKnowledgePanel} />
|
<EditorToolbarButton icon={Sparkles} label={t.quickFixes} onClick={openKnowledgePanel} />
|
||||||
<EditorToolbarButton icon={FileText} label={t.documentationMode} onClick={openDocumentationPanel} />
|
<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} />
|
<EditorToolbarButton icon={History} label={t.rollbackPlan} onClick={openSemanticDiff} />
|
||||||
|
{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>
|
<Badge tone={canApplyPayload ? "success" : "warning"}>{canApplyPayload ? t.workspaceApplyReady : t.previewRequired}</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 flex-1">
|
<div className="min-h-0 flex-1">
|
||||||
@@ -3961,6 +3975,73 @@ function formatGuardStatus(status: string, language: UiLanguage) {
|
|||||||
return status;
|
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) {
|
function sourceLocationFromNode(node?: SirNode | null) {
|
||||||
return {
|
return {
|
||||||
source_path: node?.source_path ?? null,
|
source_path: node?.source_path ?? null,
|
||||||
@@ -4226,7 +4307,12 @@ function BottomWorkbenchPanel({
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 overflow-auto border-l border-border bg-muted/20">
|
<div className="min-h-0 overflow-auto border-l border-border bg-muted/20">
|
||||||
<BottomList title={t.outputPanel} rows={outputRows} />
|
<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} />
|
<BottomList title={t.aiShort} rows={aiRows} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4287,6 +4373,8 @@ function DiffPanel({
|
|||||||
data.authoringPreview?.semantic_diff ??
|
data.authoringPreview?.semantic_diff ??
|
||||||
suggestedCode.map((text) => ({ kind: "ADD", text }));
|
suggestedCode.map((text) => ({ kind: "ADD", text }));
|
||||||
const checks = semanticPreview?.checks ?? data.authoringPreview?.checks ?? [];
|
const checks = semanticPreview?.checks ?? data.authoringPreview?.checks ?? [];
|
||||||
|
const blockedChecks = getBlockingGuardChecks(checks);
|
||||||
|
const isApplyBlocked = blockedChecks.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="min-h-0 rounded-none border-0 border-b border-border p-0 shadow-none">
|
<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">
|
<div className="mt-2 grid gap-1 text-xs text-muted-foreground">
|
||||||
<span>{semanticPreview.version_preview.next_version_id}</span>
|
<span>{semanticPreview.version_preview.next_version_id}</span>
|
||||||
<span>{t.affectedNodes}: {semanticPreview.affected_nodes.length}</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>
|
<span>{t.productionApplyDisabled}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||||
<ActionButton label={t.previewRequired} />
|
<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} />
|
<ActionButton label={t.rollbackPlan} />
|
||||||
</div>
|
</div>
|
||||||
</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="rounded-lg border border-warning/30 bg-warning/10 p-3 text-sm">
|
||||||
<div className="font-medium text-warning">{t.guardedApply}</div>
|
<div className="font-medium text-warning">{t.guardedApply}</div>
|
||||||
<div className="mt-2 grid gap-2 text-xs text-muted-foreground">
|
<div className="mt-2">
|
||||||
{checks.length === 0 ? (
|
<GuardChecklist checks={checks} emptyRows={[t.previewRequired, t.impactBeforeApply, t.reviewBeforeApply]} language={language} />
|
||||||
<>
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user