diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/public/mock/ws/v1/cluster/scheduler-conf.json b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/public/mock/ws/v1/cluster/scheduler-conf.json
index 6443fc5d72867..d0681b8f5eb91 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/public/mock/ws/v1/cluster/scheduler-conf.json
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/public/mock/ws/v1/cluster/scheduler-conf.json
@@ -292,6 +292,26 @@
"name": "yarn.scheduler.capacity.schedule-asynchronously.enable",
"value": "true"
},
+ {
+ "name": "yarn.scheduler.capacity.minimum-user-limit-percent",
+ "value": "100"
+ },
+ {
+ "name": "yarn.scheduler.capacity.user-limit-factor",
+ "value": "1"
+ },
+ {
+ "name": "yarn.scheduler.capacity.maximum-am-resource-percent",
+ "value": "0.1"
+ },
+ {
+ "name": "yarn.scheduler.capacity.root.production.maximum-applications",
+ "value": "10000"
+ },
+ {
+ "name": "yarn.scheduler.capacity.root.production.disable_preemption",
+ "value": "true"
+ },
{
"name": "yarn.scheduler.capacity.root.marketing.test.auto-create-child-queue.enabled",
"value": "true"
@@ -343,6 +363,10 @@
{
"name": "yarn.scheduler.capacity.queue-mappings",
"value": "u:user1:root.default,u:user2:root.production.prod02"
+ },
+ {
+ "name": "yarn.scheduler.capacity.global-queue-max-application",
+ "value": "5000"
}
]
}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/react-router.config.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/react-router.config.ts
index d6eefc38f8a06..7857d821df473 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/react-router.config.ts
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/react-router.config.ts
@@ -5,5 +5,5 @@ export default {
// Server-side render by default, to enable SPA mode set this to `false`
ssr: false,
appDirectory: "src/app",
- basename: "/scheduler-ui",
+ basename: "/scheduler-ui/",
} satisfies Config;
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/entry.client.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/entry.client.tsx
index 0640365b6c0d9..81f5876ab2716 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/entry.client.tsx
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/entry.client.tsx
@@ -36,6 +36,9 @@ async function enableMocking() {
// Start the worker
return worker.start({
onUnhandledRequest: 'bypass',
+ serviceWorker: {
+ url: '/scheduler-ui/mockServiceWorker.js',
+ },
});
}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/global-properties.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/global-properties.ts
index 6a457159df233..6e914e8b955e2 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/global-properties.ts
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/global-properties.ts
@@ -74,6 +74,27 @@ export const globalPropertyDefinitions: PropertyDescriptor[] = [
},
],
},
+ {
+ name: 'yarn.scheduler.capacity.global-queue-max-application',
+ displayName: 'Max Applications per Queue (Global)',
+ description:
+ 'Global per-queue maximum applications. When set, each queue is capped at this flat value (no capacity scaling). When unset, per-queue limits are calculated from Maximum Applications scaled by queue capacity.',
+ type: 'number' as PropertyType,
+ category: 'application-limits' as PropertyCategory,
+ defaultValue: '',
+ required: false,
+ validationRules: [
+ {
+ type: 'custom',
+ message: 'Must be a positive integer or empty',
+ validator: (value: string) => {
+ if (!value.trim()) return true;
+ const num = parseFloat(value);
+ return !isNaN(num) && Number.isInteger(num) && num > 0;
+ },
+ },
+ ],
+ },
{
name: 'yarn.scheduler.capacity.application.fail-fast',
displayName: 'Application Fail Fast',
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/queue-properties.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/queue-properties.ts
index f84cdfac1a11c..6fb1c87c7b663 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/queue-properties.ts
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/config/properties/queue-properties.ts
@@ -25,8 +25,14 @@ import type {
PropertyType,
PropertyCondition,
PropertyEvaluationContext,
+ InheritanceResolverContext,
} from '~/types';
import { getCapacityType } from '~/utils/capacityUtils';
+import {
+ getGlobalValue,
+ parentChainResolver,
+ globalOnlyResolver,
+} from '~/utils/resolveInheritedValue';
const LEGACY_QUEUE_MODE_PROPERTY = 'yarn.scheduler.capacity.legacy-queue-mode.enabled';
@@ -182,6 +188,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: globalOnlyResolver,
validationRules: [
{
type: 'range',
@@ -201,6 +208,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: globalOnlyResolver,
validationRules: [
{
type: 'custom',
@@ -223,6 +231,19 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: ({ configData, stagedChanges }: InheritanceResolverContext) => {
+ // YARN checks global-queue-max-application first.
+ // Only when that is unset does it fall back to maximum-applications (scaled by capacity).
+ const perQueueGlobal = getGlobalValue('global-queue-max-application', configData, stagedChanges);
+ if (perQueueGlobal !== undefined) {
+ return { value: perQueueGlobal, source: 'global' };
+ }
+ const globalMax = getGlobalValue('maximum-applications', configData, stagedChanges);
+ if (globalMax !== undefined) {
+ return { value: globalMax, source: 'global', isScaled: true };
+ }
+ return null;
+ },
validationRules: [
{
type: 'custom',
@@ -245,6 +266,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: globalOnlyResolver,
validationRules: [
{
type: 'range',
@@ -266,6 +288,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
category: 'application-limits' as PropertyCategory,
defaultValue: '',
required: false,
+ inheritanceResolver: globalOnlyResolver,
validationRules: [
{
type: 'custom',
@@ -387,6 +410,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: parentChainResolver,
validationRules: [
{
type: 'custom',
@@ -404,6 +428,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: parentChainResolver,
validationRules: [
{
type: 'custom',
@@ -422,6 +447,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: parentChainResolver,
validationRules: [
{
type: 'custom',
@@ -444,6 +470,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: parentChainResolver,
validationRules: [
{
type: 'custom',
@@ -466,6 +493,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: parentChainResolver,
},
{
name: 'intra-queue-preemption.disable_preemption',
@@ -476,6 +504,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: parentChainResolver,
},
{
name: 'priority',
@@ -557,6 +586,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
defaultValue: '',
required: false,
templateSupport: true,
+ inheritanceResolver: parentChainResolver,
validationRules: [
{
type: 'custom',
@@ -584,6 +614,7 @@ export const queuePropertyDefinitions: PropertyDescriptor[] = [
category: 'node-labels' as PropertyCategory,
defaultValue: '',
required: false,
+ inheritanceResolver: parentChainResolver,
validationRules: [
{
type: 'custom',
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyEditorTab.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyEditorTab.test.tsx
index 38217b39cdc61..cb239d1680370 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyEditorTab.test.tsx
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyEditorTab.test.tsx
@@ -89,6 +89,7 @@ const createMockPropertyEditor = () => ({
handleFieldBlur: vi.fn(),
getFieldErrors: vi.fn(() => []),
getFieldWarnings: vi.fn(() => []),
+ getInheritanceInfo: vi.fn(() => null),
properties: [
{
name: 'capacity',
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyEditorTab.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyEditorTab.tsx
index e99b1c1807bab..e99db039d45a1 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyEditorTab.tsx
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyEditorTab.tsx
@@ -107,6 +107,7 @@ export const PropertyEditorTab = ({
getFieldErrors,
getFieldWarnings,
properties,
+ getInheritanceInfo,
} = usePropertyEditor({
queuePath: queue.queuePath,
});
@@ -443,6 +444,7 @@ export const PropertyEditorTab = ({
queueName={queue.queueName}
parentQueuePath={parentQueuePath}
currentValues={watchedValues}
+ inheritanceInfo={getInheritanceInfo(prop.originalName || prop.name)}
/>
{shouldRenderTemplateButton && (
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyFieldHelpers.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyFieldHelpers.tsx
index 11d9ffd5ab290..192128c1f2532 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyFieldHelpers.tsx
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyFieldHelpers.tsx
@@ -24,7 +24,8 @@
*/
import React from 'react';
-import { Info, AlertTriangle } from 'lucide-react';
+import { Info, AlertTriangle, ArrowDown, ArrowUp } from 'lucide-react';
+import type { InheritedValueInfo } from '~/utils/resolveInheritedValue';
import { Badge } from '~/components/ui/badge';
import { FieldLabel, FieldMessage } from '~/components/ui/field';
import { Tooltip, TooltipContent, TooltipTrigger } from '~/components/ui/tooltip';
@@ -138,3 +139,56 @@ export const FieldErrorMessage: React.FC
= ({
}
return {error ? String(error.message ?? '') : inlineBusinessError};
};
+
+export interface InheritedValueIndicatorProps {
+ inheritanceInfo: InheritedValueInfo | null;
+ hasExplicitValue?: boolean;
+}
+
+export const InheritedValueIndicator: React.FC = ({
+ inheritanceInfo,
+ hasExplicitValue = false,
+}) => {
+ if (!inheritanceInfo) return null;
+
+ const sourceLabel =
+ inheritanceInfo.source === 'queue'
+ ? inheritanceInfo.sourcePath
+ : 'global default';
+
+ const scaledSuffix = inheritanceInfo.isScaled ? ' (scaled by queue capacity)' : '';
+
+ if (hasExplicitValue) {
+ return (
+
+
+
+ Overrides {sourceLabel}:{' '}
+ {inheritanceInfo.value}
+ {scaledSuffix}
+
+
+ );
+ }
+
+ return (
+
+
+
+ {inheritanceInfo.value}
+ {' '}— inherited from {sourceLabel}
+ {scaledSuffix}
+
+
+ );
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyFormField.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyFormField.tsx
index e8f2a8a6d20ed..0022e3bcc2397 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyFormField.tsx
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/PropertyFormField.tsx
@@ -28,6 +28,7 @@ import { FormField } from '~/components/ui/form';
import { Field, FieldControl, FieldDescription } from '~/components/ui/field';
import type { PropertyDescriptor } from '~/types/property-descriptor';
import { SPECIAL_VALUES } from '~/types';
+import type { InheritedValueInfo } from '~/utils/resolveInheritedValue';
import { EnumPropertyField } from './EnumPropertyField';
import { CapacityPropertyField } from './CapacityPropertyField';
import {
@@ -35,6 +36,7 @@ import {
PropertyWarnings,
BusinessErrorsList,
FieldErrorMessage,
+ InheritedValueIndicator,
} from './PropertyFieldHelpers';
import { getCommonFieldClassName, parseFieldErrors } from '../utils/fieldHelpers';
@@ -57,6 +59,7 @@ interface PropertyFormFieldProps {
parentQueuePath?: string;
currentValues?: Partial>;
setFormValue?: UseFormSetValue>;
+ inheritanceInfo?: InheritedValueInfo | null;
}
export const PropertyFormField: React.FC = ({
@@ -72,6 +75,7 @@ export const PropertyFormField: React.FC = ({
parentQueuePath,
currentValues,
setFormValue: _setFormValue,
+ inheritanceInfo,
}) => {
void _setFormValue;
@@ -136,6 +140,9 @@ export const PropertyFormField: React.FC = ({
)}
message={error ? String(error.message ?? '') : effectiveInlineError}
/>
+
>
);
@@ -175,6 +182,7 @@ export const PropertyFormField: React.FC = ({
disabled={!isEnabled}
aria-invalid={Boolean(error)}
className={commonClassName}
+ placeholder={inheritanceInfo?.value || undefined}
/>
{property.displayFormat?.suffix && (
@@ -183,6 +191,10 @@ export const PropertyFormField: React.FC = ({
)}
+
{property.description && (
{property.description}
@@ -251,7 +263,7 @@ export const PropertyFormField: React.FC = ({
onBlur?.(property.name, e.target.value);
}}
rows={2}
- placeholder={property.defaultValue || undefined}
+ placeholder={inheritanceInfo?.value || property.defaultValue || undefined}
className={cn(
'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
commonClassName,
@@ -268,7 +280,7 @@ export const PropertyFormField: React.FC = ({
field.onBlur();
onBlur?.(property.name, e.target.value);
}}
- placeholder={property.defaultValue || undefined}
+ placeholder={inheritanceInfo?.value || property.defaultValue || undefined}
disabled={!isEnabled}
aria-invalid={Boolean(error)}
className={commonClassName}
@@ -295,6 +307,10 @@ export const PropertyFormField: React.FC = ({
)}
)}
+
{property.description && (
{property.description}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/__tests__/InheritedValueIndicator.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/__tests__/InheritedValueIndicator.test.tsx
new file mode 100644
index 0000000000000..83fc6f1ad26a4
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/components/__tests__/InheritedValueIndicator.test.tsx
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '~/testing/setup/setup';
+import { InheritedValueIndicator } from '../PropertyFieldHelpers';
+
+describe('InheritedValueIndicator', () => {
+ it('renders nothing when inheritanceInfo is null', () => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('shows parent queue source with value', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/inherited from/i)).toBeInTheDocument();
+ expect(screen.getByText(/root\.production/)).toBeInTheDocument();
+ });
+
+ it('shows global default source', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/global default/i)).toBeInTheDocument();
+ });
+
+ it('shows "overrides" message when field has explicit value', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/overrides/i)).toBeInTheDocument();
+ expect(screen.getByText(/root\.production/)).toBeInTheDocument();
+ });
+
+ it('shows scaled message for scaled-from-global properties', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/root\.production/)).toBeInTheDocument();
+ expect(screen.getByText(/scaled by queue capacity/i)).toBeInTheDocument();
+ });
+
+ it('shows scaled override message when explicit and scaled-from-global', () => {
+ render(
+ ,
+ );
+ expect(screen.getByText(/overrides/i)).toBeInTheDocument();
+ expect(screen.getByText(/scaled by queue capacity/i)).toBeInTheDocument();
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.ts
index 771b793d56fad..08de908fcba2f 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.ts
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/features/property-editor/hooks/usePropertyEditor.ts
@@ -73,6 +73,7 @@ function mergeFormAndValidationErrors(
import { validatePropertyChange } from '~/features/validation/crossQueue';
import { buildPropertyKey } from '~/utils/propertyUtils';
import { CONFIG_PREFIXES } from '~/types';
+import { resolveInheritedValue, type InheritedValueInfo } from '~/utils/resolveInheritedValue';
function createFormSchema(
properties: Array<
@@ -249,6 +250,23 @@ export function usePropertyEditor({
return isStaged ? 'modified' : undefined;
};
+ const getInheritanceInfo = (propertyName: string): InheritedValueInfo | null => {
+ const property = allProperties.find(
+ (p) => p.originalName === propertyName || p.name === propertyName,
+ );
+ if (!property?.inheritanceResolver) {
+ return null;
+ }
+
+ return resolveInheritedValue({
+ queuePath,
+ propertyName,
+ configData,
+ stagedChanges,
+ inheritanceResolver: property.inheritanceResolver,
+ });
+ };
+
const stageChange = (
propertyName: string,
value: string,
@@ -613,6 +631,7 @@ export function usePropertyEditor({
propertiesByCategory,
getStagedStatus,
+ getInheritanceInfo,
properties: allProperties,
formState: form.formState,
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/lib/api/mocks/handlers.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/lib/api/mocks/handlers.ts
index ebd21700c046c..29c6b5c738c5e 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/lib/api/mocks/handlers.ts
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/lib/api/mocks/handlers.ts
@@ -24,7 +24,7 @@ import { READ_ONLY_PROPERTY } from '~/config';
// Base URL pattern that matches the API configuration
const { baseUrl, mockMode } = API_CONFIG;
-const MOCK_ASSET_BASE = '/mock/ws/v1/cluster';
+const MOCK_ASSET_BASE = `${import.meta.env.BASE_URL}mock/ws/v1/cluster`;
const staticHandlers: HttpHandler[] = [
// Scheduler endpoints - serve local mock files
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/types/property-descriptor.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/types/property-descriptor.ts
index 9a3c22ebf494a..5e90fb815c4d0 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/types/property-descriptor.ts
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/types/property-descriptor.ts
@@ -84,6 +84,22 @@ export type PropertyEvaluationContext = {
export type PropertyCondition = (context: PropertyEvaluationContext) => boolean;
+export type InheritedValueInfo = {
+ value: string;
+ source: 'queue' | 'global';
+ sourcePath?: string;
+ isScaled?: boolean;
+};
+
+export type InheritanceResolverContext = {
+ queuePath: string;
+ propertyName: string;
+ configData: Map;
+ stagedChanges?: StagedChange[];
+};
+
+export type InheritanceResolver = (context: InheritanceResolverContext) => InheritedValueInfo | null;
+
export type PropertyDescriptor = {
name: string;
displayName: string;
@@ -103,4 +119,5 @@ export type PropertyDescriptor = {
deprecationMessage?: string;
formFieldName?: string; // Escaped name for React Hook Form
originalName?: string; // Original name before escaping
+ inheritanceResolver?: InheritanceResolver;
};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/utils/resolveInheritedValue.test.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/utils/resolveInheritedValue.test.ts
new file mode 100644
index 0000000000000..e5d69fe7c08b2
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/utils/resolveInheritedValue.test.ts
@@ -0,0 +1,257 @@
+import { describe, it, expect, vi } from 'vitest';
+import {
+ resolveInheritedValue,
+ parentChainResolver,
+ globalOnlyResolver,
+} from './resolveInheritedValue';
+
+function configMap(entries: Record): Map {
+ return new Map(Object.entries(entries));
+}
+
+describe('parentChainResolver', () => {
+ it('resolves value from direct parent', () => {
+ const result = parentChainResolver({
+ queuePath: 'root.production.team1',
+ propertyName: 'disable_preemption',
+ configData: configMap({
+ 'yarn.scheduler.capacity.root.production.disable_preemption': 'true',
+ }),
+ });
+ expect(result).toEqual({
+ value: 'true',
+ source: 'queue',
+ sourcePath: 'root.production',
+ });
+ });
+
+ it('resolves value from grandparent when parent is also unset', () => {
+ const result = parentChainResolver({
+ queuePath: 'root.production.team1',
+ propertyName: 'disable_preemption',
+ configData: configMap({
+ 'yarn.scheduler.capacity.root.disable_preemption': 'true',
+ }),
+ });
+ expect(result).toEqual({
+ value: 'true',
+ source: 'queue',
+ sourcePath: 'root',
+ });
+ });
+
+ it('returns null when no ancestor has the value (no global fallback)', () => {
+ const result = parentChainResolver({
+ queuePath: 'root.production.team1',
+ propertyName: 'disable_preemption',
+ configData: configMap({}),
+ });
+ expect(result).toBeNull();
+ });
+
+ it('does NOT fall through to global property with same suffix', () => {
+ const result = parentChainResolver({
+ queuePath: 'root.production',
+ propertyName: 'disable_preemption',
+ configData: configMap({
+ 'yarn.scheduler.capacity.disable_preemption': 'true',
+ }),
+ });
+ expect(result).toBeNull();
+ });
+
+ it('still returns inherited source when queue has explicit value (for "overrides" display)', () => {
+ const result = parentChainResolver({
+ queuePath: 'root.production.team1',
+ propertyName: 'disable_preemption',
+ configData: configMap({
+ 'yarn.scheduler.capacity.root.production.team1.disable_preemption': 'false',
+ 'yarn.scheduler.capacity.root.production.disable_preemption': 'true',
+ }),
+ });
+ expect(result).toEqual({
+ value: 'true',
+ source: 'queue',
+ sourcePath: 'root.production',
+ });
+ });
+
+ it('returns null for root queue (no parent to walk to)', () => {
+ const result = parentChainResolver({
+ queuePath: 'root',
+ propertyName: 'disable_preemption',
+ configData: configMap({}),
+ });
+ expect(result).toBeNull();
+ });
+});
+
+describe('globalOnlyResolver', () => {
+ it('resolves value from global property (skips parent queues)', () => {
+ const result = globalOnlyResolver({
+ queuePath: 'root.production.team1',
+ propertyName: 'minimum-user-limit-percent',
+ configData: configMap({
+ 'yarn.scheduler.capacity.minimum-user-limit-percent': '100',
+ }),
+ });
+ expect(result).toEqual({
+ value: '100',
+ source: 'global',
+ });
+ });
+
+ it('ignores parent queue values', () => {
+ const result = globalOnlyResolver({
+ queuePath: 'root.production.team1',
+ propertyName: 'minimum-user-limit-percent',
+ configData: configMap({
+ 'yarn.scheduler.capacity.root.production.minimum-user-limit-percent': '50',
+ }),
+ });
+ expect(result).toBeNull();
+ });
+
+ it('returns null when global is not set', () => {
+ const result = globalOnlyResolver({
+ queuePath: 'root.production',
+ propertyName: 'minimum-user-limit-percent',
+ configData: configMap({}),
+ });
+ expect(result).toBeNull();
+ });
+
+ it('still returns global source when queue has explicit value', () => {
+ const result = globalOnlyResolver({
+ queuePath: 'root.production',
+ propertyName: 'minimum-user-limit-percent',
+ configData: configMap({
+ 'yarn.scheduler.capacity.root.production.minimum-user-limit-percent': '50',
+ 'yarn.scheduler.capacity.minimum-user-limit-percent': '100',
+ }),
+ });
+ expect(result).toEqual({
+ value: '100',
+ source: 'global',
+ });
+ });
+});
+
+describe('resolveInheritedValue delegation', () => {
+ it('returns null when no resolver is provided', () => {
+ const result = resolveInheritedValue({
+ queuePath: 'root.production',
+ propertyName: 'capacity',
+ configData: configMap({
+ 'yarn.scheduler.capacity.root.capacity': '100',
+ }),
+ });
+ expect(result).toBeNull();
+ });
+
+ it('delegates to the provided resolver with correct context', () => {
+ const resolver = vi.fn().mockReturnValue({ value: '500', source: 'global' as const });
+ const data = configMap({ 'yarn.scheduler.capacity.maximum-applications': '10000' });
+ const staged = [
+ {
+ id: '1',
+ queuePath: 'root.production',
+ property: 'capacity',
+ newValue: '50',
+ type: 'update' as const,
+ timestamp: Date.now(),
+ },
+ ];
+
+ resolveInheritedValue({
+ queuePath: 'root.production',
+ propertyName: 'maximum-applications',
+ configData: data,
+ stagedChanges: staged,
+ inheritanceResolver: resolver,
+ });
+
+ expect(resolver).toHaveBeenCalledWith({
+ queuePath: 'root.production',
+ propertyName: 'maximum-applications',
+ configData: data,
+ stagedChanges: staged,
+ });
+ });
+
+ it('returns resolver result unchanged', () => {
+ const expected = { value: '200', source: 'global' as const, isScaled: true };
+ const resolver = vi.fn().mockReturnValue(expected);
+
+ const result = resolveInheritedValue({
+ queuePath: 'root.production',
+ propertyName: 'maximum-applications',
+ configData: configMap({}),
+ inheritanceResolver: resolver,
+ });
+
+ expect(result).toEqual(expected);
+ });
+
+ it('returns null when resolver returns null', () => {
+ const resolver = vi.fn().mockReturnValue(null);
+
+ const result = resolveInheritedValue({
+ queuePath: 'root.production',
+ propertyName: 'maximum-applications',
+ configData: configMap({}),
+ inheritanceResolver: resolver,
+ });
+
+ expect(result).toBeNull();
+ });
+});
+
+describe('staged changes', () => {
+ it('considers parent staged changes via parentChainResolver', () => {
+ const result = resolveInheritedValue({
+ queuePath: 'root.production.team1',
+ propertyName: 'disable_preemption',
+ configData: configMap({}),
+ inheritanceResolver: parentChainResolver,
+ stagedChanges: [
+ {
+ id: '1',
+ queuePath: 'root.production',
+ property: 'disable_preemption',
+ newValue: 'true',
+ type: 'update',
+ timestamp: Date.now(),
+ },
+ ],
+ });
+ expect(result).toEqual({
+ value: 'true',
+ source: 'queue',
+ sourcePath: 'root.production',
+ });
+ });
+
+ it('considers global staged changes via globalOnlyResolver', () => {
+ const result = resolveInheritedValue({
+ queuePath: 'root.production',
+ propertyName: 'minimum-user-limit-percent',
+ configData: configMap({}),
+ inheritanceResolver: globalOnlyResolver,
+ stagedChanges: [
+ {
+ id: '1',
+ queuePath: 'global',
+ property: 'minimum-user-limit-percent',
+ newValue: '75',
+ type: 'update',
+ timestamp: Date.now(),
+ },
+ ],
+ });
+ expect(result).toEqual({
+ value: '75',
+ source: 'global',
+ });
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/utils/resolveInheritedValue.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/utils/resolveInheritedValue.ts
new file mode 100644
index 0000000000000..8f0803cd94a41
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/utils/resolveInheritedValue.ts
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { InheritanceResolver, InheritedValueInfo } from '~/types/property-descriptor';
+import type { StagedChange } from '~/types';
+import { SPECIAL_VALUES } from '~/types';
+import { buildPropertyKey, buildGlobalPropertyKey, getParentQueuePath } from './propertyUtils';
+
+export type { InheritedValueInfo } from '~/types/property-descriptor';
+
+interface ResolveOptions {
+ queuePath: string;
+ propertyName: string;
+ configData: Map;
+ stagedChanges?: StagedChange[];
+ inheritanceResolver?: InheritanceResolver;
+}
+
+export function getQueueValue(
+ queuePath: string,
+ propertyName: string,
+ configData: Map,
+ stagedChanges?: StagedChange[],
+): string | undefined {
+ const staged = stagedChanges?.find(
+ (c) => c.queuePath === queuePath && c.property === propertyName && c.newValue !== undefined,
+ );
+ if (staged) {
+ return staged.newValue;
+ }
+
+ const key = buildPropertyKey(queuePath, propertyName);
+ const value = configData.get(key);
+ return value || undefined;
+}
+
+export function getGlobalValue(
+ propertyName: string,
+ configData: Map,
+ stagedChanges?: StagedChange[],
+): string | undefined {
+ const staged = stagedChanges?.find(
+ (c) =>
+ c.queuePath === SPECIAL_VALUES.GLOBAL_QUEUE_PATH &&
+ c.property === propertyName &&
+ c.newValue !== undefined,
+ );
+ if (staged) {
+ return staged.newValue;
+ }
+
+ const key = buildGlobalPropertyKey(propertyName);
+ const value = configData.get(key);
+ return value || undefined;
+}
+
+/**
+ * Walks parent queues to find the nearest ancestor with the property set.
+ * No global fallback — the global key is often a different property entirely (e.g., preemption).
+ */
+export const parentChainResolver: InheritanceResolver = ({
+ queuePath,
+ propertyName,
+ configData,
+ stagedChanges,
+}) => {
+ let currentPath = getParentQueuePath(queuePath);
+ while (currentPath) {
+ const parentValue = getQueueValue(currentPath, propertyName, configData, stagedChanges);
+ if (parentValue !== undefined) {
+ return {
+ value: parentValue,
+ source: 'queue',
+ sourcePath: currentPath,
+ };
+ }
+ currentPath = getParentQueuePath(currentPath);
+ }
+ return null;
+};
+
+/**
+ * Checks the global yarn.scheduler.capacity. property. Skips parent queues.
+ */
+export const globalOnlyResolver: InheritanceResolver = ({
+ propertyName,
+ configData,
+ stagedChanges,
+}) => {
+ const value = getGlobalValue(propertyName, configData, stagedChanges);
+ if (value !== undefined) {
+ return { value, source: 'global' };
+ }
+ return null;
+};
+
+/**
+ * Resolves the inherited value source for a queue property by delegating
+ * to the property's inheritanceResolver. Returns null if no resolver is set
+ * or the resolver finds no inherited value.
+ */
+export function resolveInheritedValue(options: ResolveOptions): InheritedValueInfo | null {
+ const { inheritanceResolver, ...context } = options;
+ if (!inheritanceResolver) {
+ return null;
+ }
+ return inheritanceResolver(context);
+}