fix(pipelines): show the actual sandbox scope restriction (#2527)

Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
This commit is contained in:
Hyu
2026-09-11 14:42:38 +08:00
committed by GitHub
parent ff6ad6adc2
commit 1ea9cd3f6f
8 changed files with 623 additions and 65 deletions
@@ -46,30 +46,10 @@ import {
} from '@/components/ui/tooltip';
import { systemInfo } from '@/app/infra/http';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
/**
* Resolve the value referenced by a `show_if.field` string.
*
* Fields prefixed with `__system.` are looked up in the caller-supplied
* `systemContext` dictionary (e.g. `__system.is_wizard` → `systemContext.is_wizard`).
* All other field names are resolved from the live form values first, then
* fall back to `externalDependentValues`.
*/
function resolveShowIfValue(
field: string,
watchedValues: Record<string, unknown>,
externalDependentValues?: Record<string, unknown>,
systemContext?: Record<string, unknown>,
): unknown {
if (field.startsWith(SYSTEM_FIELD_PREFIX)) {
const key = field.slice(SYSTEM_FIELD_PREFIX.length);
return systemContext?.[key];
}
if (watchedValues[field] !== undefined) {
return watchedValues[field];
}
return externalDependentValues?.[field];
}
import {
resolveDisabledState,
resolveShowIfValue,
} from './DynamicFormConditions';
type DynamicFormValueSpec = Pick<
IDynamicFormItemSchema,
@@ -675,40 +655,19 @@ export default function DynamicFormComponent({
}
}
// ``disable_if`` mirrors ``show_if``'s evaluator but instead of
// hiding the field, leaves it visible and inert. Use it when the
// operator needs to see that the field exists yet cannot edit it
// under the current runtime state (e.g. sandbox-bound fields when
// Box is disabled).
let isDisabledByCondition = false;
if (config.disable_if) {
const dependValue = resolveShowIfValue(
config.disable_if.field,
// Keep locked fields visible and resolve only the applicable reason.
const { isDisabledByCondition, disabledTooltip: tooltip } =
resolveDisabledState(
config,
watchedValues as Record<string, unknown>,
externalDependentValues,
systemContext,
);
const cond = config.disable_if;
if (cond.operator === 'eq' && dependValue === cond.value) {
isDisabledByCondition = true;
} else if (cond.operator === 'neq' && dependValue !== cond.value) {
isDisabledByCondition = true;
} else if (
cond.operator === 'in' &&
Array.isArray(cond.value) &&
cond.value.includes(dependValue)
) {
isDisabledByCondition = true;
}
}
// All fields are disabled when editing (creation_settings are
// immutable) or when ``disable_if`` matches.
const isFieldDisabled = !!isEditing || isDisabledByCondition;
const disabledTooltip =
isDisabledByCondition && config.disabled_tooltip
? extractI18nObject(config.disabled_tooltip)
: '';
const disabledTooltip = tooltip ? extractI18nObject(tooltip) : '';
const renderDisabledTooltipIcon = () =>
disabledTooltip ? (
<DisabledTooltipIcon text={disabledTooltip} />
@@ -0,0 +1,71 @@
import {
SYSTEM_FIELD_PREFIX,
type IDynamicFormItemSchema,
type IShowIfCondition,
} from '@/app/infra/entities/form/dynamic';
/** System references use caller context; other fields prefer live form values. */
export function resolveShowIfValue(
field: string,
watchedValues: Record<string, unknown>,
externalDependentValues?: Record<string, unknown>,
systemContext?: Record<string, unknown>,
): unknown {
if (field.startsWith(SYSTEM_FIELD_PREFIX)) {
return systemContext?.[field.slice(SYSTEM_FIELD_PREFIX.length)];
}
if (watchedValues[field] !== undefined) {
return watchedValues[field];
}
return externalDependentValues?.[field];
}
export function matchesFormCondition(
condition: IShowIfCondition,
watchedValues: Record<string, unknown>,
externalDependentValues?: Record<string, unknown>,
systemContext?: Record<string, unknown>,
): boolean {
const value = resolveShowIfValue(
condition.field,
watchedValues,
externalDependentValues,
systemContext,
);
switch (condition.operator) {
case 'eq':
return value === condition.value;
case 'neq':
return value !== condition.value;
case 'in':
return Array.isArray(condition.value) && condition.value.includes(value);
default:
return false;
}
}
export function resolveDisabledState(
config: Pick<
IDynamicFormItemSchema,
'disable_if' | 'disabled_tooltip' | 'disabled_tooltip_overrides'
>,
watchedValues: Record<string, unknown>,
externalDependentValues?: Record<string, unknown>,
systemContext?: Record<string, unknown>,
) {
const matches = (condition: IShowIfCondition) =>
matchesFormCondition(
condition,
watchedValues,
externalDependentValues,
systemContext,
);
const isDisabledByCondition =
!!config.disable_if && matches(config.disable_if);
const disabledTooltip = isDisabledByCondition
? (config.disabled_tooltip_overrides?.find((override) =>
matches(override.when),
)?.tooltip ?? config.disabled_tooltip)
: undefined;
return { isDisabledByCondition, disabledTooltip };
}
@@ -0,0 +1,14 @@
/** Unavailability takes priority over the deployment's scope restriction. */
export function getBoxScopeContext(
boxAvailable: boolean,
forcedTemplate?: string,
) {
forcedTemplate = forcedTemplate?.trim();
return {
box_available: boxAvailable,
box_scope_editable: boxAvailable && !forcedTemplate,
// Only expose forced-scope reasons when the sandbox is available.
box_scope_forced: boxAvailable && !!forcedTemplate,
box_scope_forced_global: boxAvailable && forcedTemplate === '{global}',
};
}
@@ -8,6 +8,7 @@ import {
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import N8nAuthFormComponent from '@/app/home/components/dynamic-form/N8nAuthFormComponent';
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
import { getBoxScopeContext } from './BoxScopeContext';
import { systemInfo } from '@/app/infra/http';
import { Button } from '@/components/ui/button';
import { useForm } from 'react-hook-form';
@@ -425,13 +426,12 @@ export default function PipelineFormComponent({
// 2. the deployment pins all pipelines to a fixed scope via
// ``system.limitation.force_box_session_id_template`` (SaaS).
const forcedBoxTemplate =
systemInfo.limitation?.force_box_session_id_template || '';
systemInfo.limitation?.force_box_session_id_template?.trim() || '';
const boxScopeForced = !!forcedBoxTemplate;
const isLocalAgentStage = formName === 'ai' && stage.name === 'local-agent';
const stageSystemContext = isLocalAgentStage
? {
box_available: boxAvailable,
box_scope_editable: boxAvailable && !boxScopeForced,
...getBoxScopeContext(boxAvailable, forcedBoxTemplate),
pipeline_id: pipelineId,
}
: undefined;
@@ -39,6 +39,13 @@ export interface IDynamicFormItemSchema {
disable_if?: IShowIfCondition;
/** Tooltip shown next to the field label when ``disable_if`` is active. */
disabled_tooltip?: I18nObject;
/** Optional overrides evaluated in order when ``disable_if`` matches.
* The first matching ``when`` wins; otherwise use ``disabled_tooltip``.
* Conditions use the same operators and value lookup as ``disable_if``. */
disabled_tooltip_overrides?: {
when: IShowIfCondition;
tooltip: I18nObject;
}[];
/** when type is PLUGIN_SELECTOR, the scopes is the scopes of components(plugin contains), the default is all */
scopes?: string[];