+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes.ts
new file mode 100644
index 0000000000000..4dd22d339a71a
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 RouteConfig, index, layout, route } from '@react-router/dev/routes';
+
+export default [
+ layout('routes/layout.tsx', [
+ index('routes/home.tsx'),
+ route('node-labels', 'routes/node-labels.tsx'),
+ route('placement-rules', 'routes/placement-rules.tsx'),
+ route('global-settings', 'routes/global-settings.tsx'),
+ ]),
+] satisfies RouteConfig;
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/__tests__/home.integration.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/__tests__/home.integration.test.tsx
new file mode 100644
index 0000000000000..d60866a93e7ea
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/__tests__/home.integration.test.tsx
@@ -0,0 +1,97 @@
+/**
+ * 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, vi, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router';
+import Home from '~/app/routes/home';
+import { ThemeProvider } from '~/components/providers/theme-provider';
+
+// Mock the feature components
+vi.mock('~/features/queue-management/components/QueueVisualizationContainer', () => ({
+ QueueVisualizationContainer: () => (
+
,
+}));
+
+// Mock the store
+vi.mock('~/stores/schedulerStore', () => ({
+ useSchedulerStore: vi.fn(() => ({
+ schedulerData: {
+ type: 'capacityScheduler',
+ capacity: 100,
+ usedCapacity: 0,
+ maxCapacity: 100,
+ queueName: 'root',
+ queues: {
+ queue: [],
+ },
+ },
+ selectedQueuePath: null,
+ isPropertyPanelOpen: false,
+ stagedChanges: [],
+ isLoading: false,
+ error: null,
+ selectQueue: vi.fn(),
+ loadInitialData: vi.fn(),
+ })),
+}));
+
+describe('Home route', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const renderHome = () => {
+ return render(
+
+
+
+
+ ,
+ );
+ };
+
+ it('should render without crashing', () => {
+ renderHome();
+ expect(screen.getByTestId('queue-visualization')).toBeInTheDocument();
+ });
+
+ it('should render QueueVisualizationContainer', () => {
+ renderHome();
+ expect(screen.getByTestId('queue-visualization')).toBeInTheDocument();
+ expect(screen.getByText('Queue Visualization')).toBeInTheDocument();
+ });
+
+ it('should render PropertyPanel', () => {
+ renderHome();
+ expect(screen.getByTestId('property-panel')).toBeInTheDocument();
+ expect(screen.getByText('Property Panel')).toBeInTheDocument();
+ });
+
+ it('should render both main components', () => {
+ renderHome();
+ expect(screen.getByTestId('queue-visualization')).toBeInTheDocument();
+ expect(screen.getByTestId('property-panel')).toBeInTheDocument();
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/global-settings.meta.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/global-settings.meta.ts
new file mode 100644
index 0000000000000..eb04df2482ae2
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/global-settings.meta.ts
@@ -0,0 +1,27 @@
+/**
+ * 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 { Route } from './+types/global-settings';
+
+export function meta(_args: Route.MetaArgs) {
+ return [
+ { title: 'Global Settings - YARN Scheduler UI' },
+ { name: 'description', content: 'Configure scheduler-wide capacity settings and properties' },
+ ];
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/global-settings.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/global-settings.tsx
new file mode 100644
index 0000000000000..50ad5f0d2689d
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/global-settings.tsx
@@ -0,0 +1,33 @@
+/**
+ * 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 { GlobalSettings } from '~/features/global-settings/components/GlobalSettings';
+
+// eslint-disable-next-line react-refresh/only-export-components
+export { meta } from './global-settings.meta';
+
+export default function GlobalSettingsRoute() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/home.meta.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/home.meta.ts
new file mode 100644
index 0000000000000..35a60bf903c9f
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/home.meta.ts
@@ -0,0 +1,27 @@
+/**
+ * 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 { Route } from './+types/home';
+
+export function meta(_args: Route.MetaArgs) {
+ return [
+ { title: 'YARN Capacity Scheduler UI' },
+ { name: 'description', content: 'Configuration UI for YARN Capacity Scheduler' },
+ ];
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/home.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/home.tsx
new file mode 100644
index 0000000000000..e5012e673e4c7
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/home.tsx
@@ -0,0 +1,33 @@
+/**
+ * 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 { QueueVisualizationContainer } from '~/features/queue-management/components/QueueVisualizationContainer';
+import { PropertyPanel } from '~/features/property-editor/components/PropertyPanel';
+
+// eslint-disable-next-line react-refresh/only-export-components
+export { meta } from './home.meta';
+
+export default function Home() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/layout.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/layout.tsx
new file mode 100644
index 0000000000000..a8587c5fb30f0
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/layout.tsx
@@ -0,0 +1,241 @@
+/**
+ * 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 { Outlet, useLocation } from 'react-router';
+import { useState, useEffect } from 'react';
+import { useSchedulerStore } from '~/stores/schedulerStore';
+import { StagedChangesPanel } from '~/features/staged-changes/components/StagedChangesPanel';
+import { AppSidebar } from '~/components/layouts/app-sidebar';
+import { ModeToggle } from '~/components/elements/mode-toggle';
+import { GlobalRefreshButton } from '~/components/elements/GlobalRefreshButton';
+import { DiagnosticsDialog } from '~/components/elements/DiagnosticsDialog';
+import { SidebarProvider, SidebarInset, SidebarTrigger } from '~/components/ui/sidebar';
+import { Badge } from '~/components/ui/badge';
+import { Button } from '~/components/ui/button';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui/tooltip';
+import { Lock, GitCompareArrows } from 'lucide-react';
+import { SearchBar } from '~/components/search/SearchBar';
+import { useKeyboardShortcuts } from '~/hooks/useKeyboardShortcuts';
+import { toast } from 'sonner';
+import { READ_ONLY_PROPERTY } from '~/config';
+
+export default function Layout() {
+ const [stagedChangesPanelOpen, setStagedChangesPanelOpen] = useState(false);
+ const [isApplying, setIsApplying] = useState(false);
+ const loadInitialData = useSchedulerStore((state) => state.loadInitialData);
+ const stagedChanges = useSchedulerStore((state) => state.stagedChanges);
+ const setSearchContext = useSchedulerStore((state) => state.setSearchContext);
+ const isReadOnly = useSchedulerStore((state) => state.isReadOnly);
+ const applyChanges = useSchedulerStore((state) => state.applyChanges);
+ const clearAllChanges = useSchedulerStore((state) => state.clearAllChanges);
+ const isPropertyPanelOpen = useSchedulerStore((state) => state.isPropertyPanelOpen);
+ const isComparisonModeActive = useSchedulerStore((state) => state.isComparisonModeActive);
+ const toggleComparisonMode = useSchedulerStore((state) => state.toggleComparisonMode);
+ const location = useLocation();
+
+ useEffect(() => {
+ loadInitialData().catch((err) => {
+ console.error('Failed to load initial data:', err);
+ });
+ }, [loadInitialData]);
+
+ // Update search context based on current route
+ useEffect(() => {
+ if (location.pathname === '/') {
+ setSearchContext('queues');
+ } else if (location.pathname === '/node-labels') {
+ setSearchContext('nodes');
+ } else if (location.pathname === '/global-settings') {
+ setSearchContext('settings');
+ } else {
+ setSearchContext(null);
+ }
+ }, [location.pathname, setSearchContext]);
+
+ // Calculate if there are validation errors blocking apply
+ const hasValidationErrors = stagedChanges.some((change) =>
+ change.validationErrors?.some((error) => error.severity === 'error'),
+ );
+
+ // Global keyboard shortcuts for staged changes
+ useKeyboardShortcuts([
+ {
+ key: 's',
+ ctrl: true,
+ meta: true,
+ preventDefault: true,
+ handler: async () => {
+ // Don't trigger if property panel is open (let PropertyPanel handle it)
+ if (isPropertyPanelOpen) {
+ return;
+ }
+
+ // If no staged changes, inform user
+ if (stagedChanges.length === 0) {
+ return;
+ }
+
+ // If panel is closed, open it
+ if (!stagedChangesPanelOpen) {
+ setStagedChangesPanelOpen(true);
+ toast.info('Staged changes panel opened. Press Cmd/Ctrl+S again to apply changes.');
+ return;
+ }
+
+ // If panel is open and there are changes without validation errors, apply them
+ if (!isApplying && !hasValidationErrors && !isReadOnly) {
+ setIsApplying(true);
+ try {
+ await applyChanges();
+ toast.success('All changes applied successfully');
+ setStagedChangesPanelOpen(false);
+ } catch (error) {
+ toast.error('Failed to apply changes');
+ console.error('Failed to apply changes:', error);
+ } finally {
+ setIsApplying(false);
+ }
+ } else if (hasValidationErrors) {
+ toast.error('Cannot apply changes with validation errors');
+ } else if (isReadOnly) {
+ toast.error('Cannot apply changes in read-only mode');
+ }
+ },
+ },
+ {
+ key: 'k',
+ ctrl: true,
+ meta: true,
+ preventDefault: true,
+ handler: () => {
+ // Only trigger when staged changes panel is open and property panel is not
+ if (stagedChangesPanelOpen && !isPropertyPanelOpen && stagedChanges.length > 0) {
+ clearAllChanges();
+ toast.info('All staged changes cleared');
+ }
+ },
+ },
+ ]);
+
+ // Determine page title and description based on current route
+ const getPageInfo = () => {
+ if (location.pathname === '/') {
+ return {
+ title: 'Queue Hierarchy',
+ description: 'Visualize and manage your YARN Capacity Scheduler queues',
+ };
+ } else if (location.pathname === '/global-settings') {
+ return {
+ title: 'Global Settings',
+ description: 'Configure scheduler-wide settings and properties',
+ };
+ } else if (location.pathname === '/node-labels') {
+ return {
+ title: 'Node Labels',
+ description: 'Manage node labels and node-to-label mappings',
+ };
+ } else if (location.pathname === '/placement-rules') {
+ return {
+ title: 'Placement Rules',
+ description: 'Define rules for application placement in queues',
+ };
+ }
+ return { title: '', description: '' };
+ };
+
+ const pageInfo = getPageInfo();
+
+ return (
+
+
+
+
+
+
+ setStagedChangesPanelOpen(false)}
+ onOpen={() => setStagedChangesPanelOpen(true)}
+ />
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/node-labels.meta.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/node-labels.meta.ts
new file mode 100644
index 0000000000000..82a1b158414dc
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/node-labels.meta.ts
@@ -0,0 +1,30 @@
+/**
+ * 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 { Route } from './+types/node-labels';
+
+export function meta(_args: Route.MetaArgs) {
+ return [
+ { title: 'Node Labels - YARN Scheduler UI' },
+ {
+ name: 'description',
+ content: 'Manage node labels and partition configurations for YARN cluster',
+ },
+ ];
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/node-labels.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/node-labels.tsx
new file mode 100644
index 0000000000000..bd381b865fdd3
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/node-labels.tsx
@@ -0,0 +1,31 @@
+/**
+ * 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 { NodeLabels } from '~/features/node-labels/components/NodeLabels';
+
+// eslint-disable-next-line react-refresh/only-export-components
+export { meta } from './node-labels.meta';
+
+export default function NodeLabelsRoute() {
+ return (
+
+
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/placement-rules.meta.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/placement-rules.meta.ts
new file mode 100644
index 0000000000000..79ce068f4bbfe
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/placement-rules.meta.ts
@@ -0,0 +1,27 @@
+/**
+ * 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 { MetaFunction } from 'react-router';
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: 'Placement Rules - YARN Scheduler UI' },
+ { name: 'description', content: 'Configure queue placement rules for YARN applications' },
+ ];
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/placement-rules.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/placement-rules.tsx
new file mode 100644
index 0000000000000..9ada07b980590
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/app/routes/placement-rules.tsx
@@ -0,0 +1,28 @@
+/**
+ * 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 { PlacementRules } from '~/features/placement-rules/components/PlacementRules';
+
+export default function PlacementRulesRoute() {
+ return (
+
+
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/DiagnosticsDialog.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/DiagnosticsDialog.test.tsx
new file mode 100644
index 0000000000000..90cec69e9a33a
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/DiagnosticsDialog.test.tsx
@@ -0,0 +1,157 @@
+/**
+ * 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 { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { DiagnosticsDialog } from './DiagnosticsDialog';
+import { useSchedulerStore } from '~/stores/schedulerStore';
+
+vi.mock('~/stores/schedulerStore');
+
+// Mock UI primitives that rely on portals or complex behaviors
+vi.mock('~/components/ui/dialog', () => ({
+ Dialog: ({ children }: any) =>
{children}
,
+ DialogTrigger: ({ children }: any) => <>{children}>,
+ DialogContent: ({ children }: any) =>
{children}
,
+ DialogHeader: ({ children }: any) =>
{children}
,
+ DialogTitle: ({ children }: any) =>
{children}
,
+ DialogDescription: ({ children }: any) =>
{children}
,
+ DialogFooter: ({ children }: any) =>
{children}
,
+}));
+
+vi.mock('~/components/ui/tooltip', () => ({
+ TooltipProvider: ({ children }: any) => <>{children}>,
+ TooltipTrigger: ({ children }: any) => <>{children}>,
+ TooltipContent: ({ children }: any) => <>{children}>,
+ Tooltip: ({ children }: any) => <>{children}>,
+}));
+
+vi.mock('~/components/ui/checkbox', () => ({
+ Checkbox: ({ id, checked, onCheckedChange }: any) => (
+ onCheckedChange?.(event.currentTarget.checked)}
+ />
+ ),
+}));
+
+describe('DiagnosticsDialog', () => {
+ const createStoreState = () => ({
+ configData: new Map([
+ ['yarn.scheduler.capacity.root.capacity', '100'],
+ ['yarn.scheduler.capacity.root.default.capacity', '50'],
+ ]),
+ configVersion: 7,
+ schedulerData: { queueName: 'root', queuePath: 'root', queues: { queue: [] } } as any,
+ nodeLabels: [{ name: 'x', exclusivity: true }],
+ nodeToLabels: [{ nodeId: 'node-1', nodeLabels: ['x'] }],
+ nodes: [],
+ });
+
+ let storeState: ReturnType;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ storeState = createStoreState();
+ vi.mocked(useSchedulerStore).mockImplementation((selector: any) => selector(storeState));
+ });
+
+ it('disables download when no datasets are selected', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const downloadButton = screen.getByRole('button', { name: /^download$/i });
+ expect(downloadButton).not.toBeDisabled();
+
+ await user.click(screen.getByLabelText('Scheduler Configuration'));
+ await user.click(screen.getByLabelText('Scheduler Info'));
+
+ expect(downloadButton).toBeDisabled();
+
+ await user.click(screen.getByLabelText('Node Labels'));
+ expect(downloadButton).not.toBeDisabled();
+ });
+
+ it('creates a downloadable diagnostics bundle with selected datasets', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const originalCreateObjectURL = URL.createObjectURL;
+ const originalRevokeObjectURL = URL.revokeObjectURL;
+ const createObjectURLMock = vi.fn(() => 'blob:url');
+ const revokeObjectURLMock = vi.fn();
+
+ Object.assign(URL, {
+ createObjectURL: createObjectURLMock,
+ revokeObjectURL: revokeObjectURLMock,
+ });
+
+ const originalCreateElement = document.createElement.bind(document);
+ const anchorElement = originalCreateElement('a');
+ const clickSpy = vi.spyOn(anchorElement, 'click').mockImplementation(() => {});
+ const createElementMock = vi
+ .spyOn(document, 'createElement')
+ .mockImplementation((tagName: string) => {
+ if (tagName === 'a') {
+ return anchorElement;
+ }
+ return originalCreateElement(tagName);
+ });
+
+ const downloadButton = screen.getByRole('button', { name: /^download$/i });
+
+ try {
+ await user.click(downloadButton);
+
+ expect(createObjectURLMock).toHaveBeenCalledTimes(1);
+ const firstCall = createObjectURLMock.mock.calls[0] as unknown[] | undefined;
+ expect(firstCall).toBeDefined();
+ const blobArg = firstCall?.[0];
+ expect(blobArg).toBeInstanceOf(Blob);
+ const payloadText = await (blobArg as Blob).text();
+ const payload = JSON.parse(payloadText);
+
+ expect(revokeObjectURLMock).toHaveBeenCalledWith('blob:url');
+ expect(clickSpy).toHaveBeenCalledTimes(1);
+ expect(anchorElement.href).toBe('blob:url');
+ expect(anchorElement.download).toMatch(/^yarn-diagnostics-.*\.json$/);
+ expect(Object.keys(payload.datasets)).toEqual(['schedulerConf', 'schedulerInfo']);
+ const expectedProperties = Object.fromEntries(
+ Array.from(storeState.configData.entries()).sort(([a], [b]) => a.localeCompare(b)),
+ );
+ expect(payload.datasets.schedulerConf).toEqual({
+ version: storeState.configVersion,
+ properties: expectedProperties,
+ });
+ expect(payload.datasets.schedulerInfo).toEqual(storeState.schedulerData);
+ } finally {
+ createElementMock.mockRestore();
+ clickSpy.mockRestore();
+ Object.assign(URL, {
+ createObjectURL: originalCreateObjectURL,
+ revokeObjectURL: originalRevokeObjectURL,
+ });
+ }
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/DiagnosticsDialog.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/DiagnosticsDialog.tsx
new file mode 100644
index 0000000000000..c3b921a897fe0
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/DiagnosticsDialog.tsx
@@ -0,0 +1,205 @@
+/**
+ * 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 { useState } from 'react';
+import { FileDown } from 'lucide-react';
+
+import { Button } from '~/components/ui/button';
+import { Checkbox } from '~/components/ui/checkbox';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '~/components/ui/dialog';
+import { Label } from '~/components/ui/label';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui/tooltip';
+import { useSchedulerStore } from '~/stores/schedulerStore';
+
+type DiagnosticDatasetId =
+ | 'schedulerConf'
+ | 'schedulerInfo'
+ | 'nodeLabels'
+ | 'nodeToLabels'
+ | 'nodes';
+
+interface DiagnosticOption {
+ id: DiagnosticDatasetId;
+ label: string;
+ description: string;
+ data: unknown;
+}
+
+const DEFAULT_SELECTED: DiagnosticDatasetId[] = ['schedulerConf', 'schedulerInfo'];
+
+export function DiagnosticsDialog() {
+ const configData = useSchedulerStore((state) => state.configData);
+ const configVersion = useSchedulerStore((state) => state.configVersion);
+ const schedulerData = useSchedulerStore((state) => state.schedulerData);
+ const nodeLabels = useSchedulerStore((state) => state.nodeLabels);
+ const nodeToLabels = useSchedulerStore((state) => state.nodeToLabels);
+ const nodes = useSchedulerStore((state) => state.nodes);
+
+ const [open, setOpen] = useState(false);
+ const [selectedDatasets, setSelectedDatasets] = useState(DEFAULT_SELECTED);
+
+ const entries = Array.from(configData.entries()).sort(([a], [b]) => a.localeCompare(b));
+ const schedulerConfiguration = {
+ version: configVersion,
+ properties: Object.fromEntries(entries),
+ };
+
+ const datasetOptions: DiagnosticOption[] = [
+ {
+ id: 'schedulerConf',
+ label: 'Scheduler Configuration',
+ description: 'Key/value pairs returned by /scheduler-conf (including version metadata).',
+ data: schedulerConfiguration,
+ },
+ {
+ id: 'schedulerInfo',
+ label: 'Scheduler Info',
+ description: 'Current scheduler metrics returned by /scheduler.',
+ data: schedulerData,
+ },
+ {
+ id: 'nodeLabels',
+ label: 'Node Labels',
+ description: 'Label definitions from /node-labels.',
+ data: nodeLabels,
+ },
+ {
+ id: 'nodeToLabels',
+ label: 'Node-to-Labels Mapping',
+ description: 'Assignments from /node-to-labels.',
+ data: nodeToLabels,
+ },
+ {
+ id: 'nodes',
+ label: 'Nodes',
+ description: 'Node metadata returned by /nodes.',
+ data: nodes,
+ },
+ ];
+
+ const toggleDataset = (datasetId: DiagnosticDatasetId, checked: boolean) => {
+ setSelectedDatasets((prev) => {
+ if (checked) {
+ return prev.includes(datasetId) ? prev : [...prev, datasetId];
+ }
+ return prev.filter((id) => id !== datasetId);
+ });
+ };
+
+ const handleDownload = () => {
+ if (selectedDatasets.length === 0) {
+ return;
+ }
+
+ const timestamp = new Date().toISOString();
+ const payload: Record = {
+ generatedAt: timestamp,
+ datasets: {},
+ };
+
+ for (const option of datasetOptions) {
+ if (selectedDatasets.includes(option.id)) {
+ (payload.datasets as Record)[option.id] = option.data;
+ }
+ }
+
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = `yarn-diagnostics-${timestamp.replace(/[:.]/g, '-')}.json`;
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+ URL.revokeObjectURL(url);
+ setOpen(false);
+ };
+
+ const isDownloadDisabled = selectedDatasets.length === 0;
+
+ return (
+
+
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/GlobalRefreshButton.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/GlobalRefreshButton.test.tsx
new file mode 100644
index 0000000000000..91ab9f758d64a
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/GlobalRefreshButton.test.tsx
@@ -0,0 +1,68 @@
+/**
+ * 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 { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { GlobalRefreshButton } from './GlobalRefreshButton';
+import { useSchedulerStore } from '~/stores/schedulerStore';
+
+vi.mock('~/stores/schedulerStore');
+
+describe('GlobalRefreshButton', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('calls loadInitialData when clicked', async () => {
+ const loadInitialData = vi.fn().mockResolvedValue(undefined);
+ const state = {
+ loadInitialData,
+ isLoading: false,
+ };
+
+ vi.mocked(useSchedulerStore).mockImplementation((selector: any) => selector(state));
+
+ const user = userEvent.setup();
+ render();
+
+ const button = screen.getByRole('button', { name: /refresh data/i });
+ await user.click(button);
+
+ expect(loadInitialData).toHaveBeenCalledTimes(1);
+ });
+
+ it('disables the button and shows spinner while loading', () => {
+ const loadInitialData = vi.fn().mockResolvedValue(undefined);
+ const state = {
+ loadInitialData,
+ isLoading: true,
+ };
+
+ vi.mocked(useSchedulerStore).mockImplementation((selector: any) => selector(state));
+
+ render();
+
+ const button = screen.getByRole('button', { name: /refresh data/i });
+ expect(button).toBeDisabled();
+
+ const icon = button.querySelector('svg');
+ expect(icon).toHaveClass('animate-spin');
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/GlobalRefreshButton.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/GlobalRefreshButton.tsx
new file mode 100644
index 0000000000000..aa8b0f387b30e
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/GlobalRefreshButton.tsx
@@ -0,0 +1,56 @@
+/**
+ * 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 { RefreshCw } from 'lucide-react';
+
+import { Button } from '~/components/ui/button';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui/tooltip';
+import { useSchedulerStore } from '~/stores/schedulerStore';
+
+export function GlobalRefreshButton() {
+ const loadInitialData = useSchedulerStore((state) => state.loadInitialData);
+ const isLoading = useSchedulerStore((state) => state.isLoading);
+
+ const handleRefresh = async () => {
+ try {
+ await loadInitialData();
+ } catch (error) {
+ console.error('Failed to refresh scheduler data:', error);
+ }
+ };
+
+ return (
+
+
+
+
+
+ Refresh data
+
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/mode-toggle.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/mode-toggle.test.tsx
new file mode 100644
index 0000000000000..9bbc0b7ee3be0
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/mode-toggle.test.tsx
@@ -0,0 +1,342 @@
+/**
+ * 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 { render, screen } from '~/testing/setup/setup';
+import userEvent from '@testing-library/user-event';
+import { vi } from 'vitest';
+import { ModeToggle } from './mode-toggle';
+
+// Mock the useTheme hook
+const mockSetTheme = vi.fn();
+const mockUseTheme = vi.fn(() => ({
+ theme: 'light',
+ setTheme: mockSetTheme,
+}));
+
+vi.mock('~/components/providers/use-theme', () => ({
+ useTheme: () => mockUseTheme(),
+}));
+
+describe('ModeToggle', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('Display and Accessibility', () => {
+ it('should render the toggle button with proper accessibility label', () => {
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ expect(button).toBeInTheDocument();
+ });
+
+ it('should display both sun and moon icons', () => {
+ render();
+
+ // Both icons should be in the DOM
+ const sunIcon = document.querySelector('.lucide-sun');
+ const moonIcon = document.querySelector('.lucide-moon');
+
+ expect(sunIcon).toBeInTheDocument();
+ expect(moonIcon).toBeInTheDocument();
+ });
+
+ it('should show sun icon in light mode with correct classes', () => {
+ render();
+
+ const sunIcon = document.querySelector('.lucide-sun');
+ const moonIcon = document.querySelector('.lucide-moon');
+
+ // Sun should be visible in light mode
+ expect(sunIcon).toHaveClass('rotate-0', 'scale-100');
+ // Moon should be hidden in light mode
+ expect(moonIcon).toHaveClass('rotate-90', 'scale-0');
+ });
+
+ it('should show moon icon in dark mode with correct classes', () => {
+ // Re-mock with dark theme
+ mockUseTheme.mockReturnValue({
+ theme: 'dark',
+ setTheme: mockSetTheme,
+ });
+
+ render();
+
+ const sunIcon = document.querySelector('.lucide-sun');
+ const moonIcon = document.querySelector('.lucide-moon');
+
+ // In dark mode, these classes control visibility through CSS
+ expect(sunIcon).toHaveClass('dark:-rotate-90', 'dark:scale-0');
+ expect(moonIcon).toHaveClass('dark:rotate-0', 'dark:scale-100');
+ });
+ });
+
+ describe('Dropdown Menu Functionality', () => {
+ it('should not show dropdown menu initially', () => {
+ render();
+
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument();
+ });
+
+ it('should open dropdown menu when button is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ // Check that all menu items are visible
+ expect(screen.getByRole('menuitem', { name: /light/i })).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: /dark/i })).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: /system/i })).toBeInTheDocument();
+ });
+
+ it('should close dropdown menu after interaction', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open menu
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+ expect(screen.getByRole('menuitem', { name: /light/i })).toBeInTheDocument();
+
+ // Select an item to close the menu
+ await user.click(screen.getByRole('menuitem', { name: /light/i }));
+
+ // Menu should be closed after selection
+ expect(screen.queryByRole('menuitem', { name: /light/i })).not.toBeInTheDocument();
+
+ // Verify we can open it again
+ await user.click(button);
+ expect(screen.getByRole('menuitem', { name: /light/i })).toBeInTheDocument();
+ });
+
+ it('should close dropdown menu when escape key is pressed', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open menu
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+ expect(screen.getByRole('menuitem', { name: /light/i })).toBeInTheDocument();
+
+ // Press escape
+ await user.keyboard('{Escape}');
+
+ // Menu should be closed
+ expect(screen.queryByRole('menuitem', { name: /light/i })).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Theme Switching Behavior', () => {
+ it('should call setTheme with "light" when Light option is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open menu
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ // Click Light option
+ const lightOption = screen.getByRole('menuitem', { name: /light/i });
+ await user.click(lightOption);
+
+ expect(mockSetTheme).toHaveBeenCalledWith('light');
+ expect(mockSetTheme).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call setTheme with "dark" when Dark option is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open menu
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ // Click Dark option
+ const darkOption = screen.getByRole('menuitem', { name: /dark/i });
+ await user.click(darkOption);
+
+ expect(mockSetTheme).toHaveBeenCalledWith('dark');
+ expect(mockSetTheme).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call setTheme with "system" when System option is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open menu
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ // Click System option
+ const systemOption = screen.getByRole('menuitem', { name: /system/i });
+ await user.click(systemOption);
+
+ expect(mockSetTheme).toHaveBeenCalledWith('system');
+ expect(mockSetTheme).toHaveBeenCalledTimes(1);
+ });
+
+ it('should close menu after selecting a theme option', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open menu
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ // Click Light option
+ const lightOption = screen.getByRole('menuitem', { name: /light/i });
+ await user.click(lightOption);
+
+ // Menu should be closed
+ expect(screen.queryByRole('menuitem', { name: /light/i })).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Integration with ThemeProvider', () => {
+ it('should use the useTheme hook from theme provider', () => {
+ render();
+
+ // The component renders successfully, which means useTheme hook is being used
+ expect(screen.getByRole('button', { name: /toggle theme/i })).toBeInTheDocument();
+ });
+
+ it('should work with different initial theme values', () => {
+ // Test with system theme
+ mockUseTheme.mockReturnValue({
+ theme: 'system',
+ setTheme: mockSetTheme,
+ });
+
+ const { rerender } = render();
+ expect(screen.getByRole('button', { name: /toggle theme/i })).toBeInTheDocument();
+
+ // Test with dark theme
+ mockUseTheme.mockReturnValue({
+ theme: 'dark',
+ setTheme: mockSetTheme,
+ });
+
+ rerender();
+ expect(screen.getByRole('button', { name: /toggle theme/i })).toBeInTheDocument();
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('should handle multiple theme changes in sequence', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+
+ // First interaction - set to dark
+ await user.click(button);
+ await user.click(screen.getByRole('menuitem', { name: /dark/i }));
+ expect(mockSetTheme).toHaveBeenCalledWith('dark');
+
+ // Second interaction - set to system
+ await user.click(button);
+ await user.click(screen.getByRole('menuitem', { name: /system/i }));
+ expect(mockSetTheme).toHaveBeenCalledWith('system');
+
+ // Third interaction - set to light
+ await user.click(button);
+ await user.click(screen.getByRole('menuitem', { name: /light/i }));
+ expect(mockSetTheme).toHaveBeenCalledWith('light');
+
+ // Verify all calls were made
+ expect(mockSetTheme).toHaveBeenCalledTimes(3);
+ });
+
+ it('should handle theme changes while menu is open', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open menu
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ // Change theme while menu is open
+ mockUseTheme.mockReturnValue({
+ theme: 'dark',
+ setTheme: mockSetTheme,
+ });
+
+ // Click dark option
+ await user.click(screen.getByRole('menuitem', { name: /dark/i }));
+
+ expect(mockSetTheme).toHaveBeenCalledWith('dark');
+ });
+
+ it('should maintain button focus after closing menu with escape', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+
+ // Focus and open menu
+ button.focus();
+ await user.click(button);
+
+ // Close with escape
+ await user.keyboard('{Escape}');
+
+ // Button should still have focus
+ expect(button).toHaveFocus();
+ });
+ });
+
+ describe('Keyboard Navigation', () => {
+ it('should support keyboard navigation in dropdown menu', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open menu with keyboard
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ button.focus();
+ await user.keyboard('{Enter}');
+
+ // Menu should be open
+ expect(screen.getByRole('menuitem', { name: /light/i })).toBeInTheDocument();
+
+ // Navigate with arrow keys
+ await user.keyboard('{ArrowDown}');
+ expect(screen.getByRole('menuitem', { name: /dark/i })).toHaveFocus();
+
+ await user.keyboard('{ArrowDown}');
+ expect(screen.getByRole('menuitem', { name: /system/i })).toHaveFocus();
+
+ // Select with Enter
+ await user.keyboard('{Enter}');
+ expect(mockSetTheme).toHaveBeenCalledWith('system');
+ });
+
+ it('should support space key to open menu', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ button.focus();
+ await user.keyboard(' ');
+
+ expect(screen.getByRole('menuitem', { name: /light/i })).toBeInTheDocument();
+ });
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/mode-toggle.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/mode-toggle.tsx
new file mode 100644
index 0000000000000..4c528787aa2ee
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/elements/mode-toggle.tsx
@@ -0,0 +1,50 @@
+/**
+ * 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 { Moon, Sun } from 'lucide-react';
+
+import { Button } from '~/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '~/components/ui/dropdown-menu';
+import { useTheme } from '~/components/providers/use-theme';
+
+export function ModeToggle() {
+ const { setTheme } = useTheme();
+
+ return (
+
+
+
+
+
+ setTheme('light')}>Light
+ setTheme('dark')}>Dark
+ setTheme('system')}>System
+
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/layouts/app-sidebar.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/layouts/app-sidebar.test.tsx
new file mode 100644
index 0000000000000..343c6495b42ea
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/layouts/app-sidebar.test.tsx
@@ -0,0 +1,246 @@
+/**
+ * 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 React from 'react';
+import { render, screen } from '@testing-library/react';
+import { vi } from 'vitest';
+import { AppSidebar } from './app-sidebar';
+
+// Mock React Router
+const mockLocation = { pathname: '/' };
+const mockNavigate = vi.fn();
+
+vi.mock('react-router', () => ({
+ Link: ({ children, to, ...props }: any) => (
+
+ {children}
+
+ ),
+ useLocation: () => mockLocation,
+ useNavigate: () => mockNavigate,
+}));
+
+// Mock the Sidebar UI components to focus on testing AppSidebar behavior
+vi.mock('~/components/ui/sidebar', () => ({
+ Sidebar: ({ children, ...props }: any) => (
+
+ ),
+ SidebarContent: ({ children }: any) =>
{children}
,
+ SidebarHeader: ({ children }: any) => {children},
+ SidebarMenu: ({ children }: any) => ,
+ SidebarMenuItem: ({ children }: any) =>
{children}
,
+ SidebarMenuButton: ({ children, isActive, asChild, ...props }: any) => {
+ const className = isActive ? 'active' : '';
+ if (asChild && React.isValidElement(children)) {
+ // eslint-disable-next-line @eslint-react/no-clone-element
+ return React.cloneElement(children as React.ReactElement, {
+ className: `${(children.props as any).className || ''} ${className}`.trim(),
+ 'data-active': isActive,
+ });
+ }
+ return (
+
+ );
+ },
+}));
+
+describe('AppSidebar', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Reset location to default
+ mockLocation.pathname = '/';
+ });
+
+ describe('Header', () => {
+ it('should display the application title', () => {
+ render();
+
+ expect(screen.getByText('Capacity Scheduler UI')).toBeInTheDocument();
+ });
+ });
+
+ describe('Navigation Links', () => {
+ it('should render all navigation items', () => {
+ render();
+
+ expect(screen.getByRole('link', { name: /Queues/i })).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: /Node Labels/i })).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: /Global Settings/i })).toBeInTheDocument();
+ });
+
+ it('should have correct href attributes for navigation links', () => {
+ render();
+
+ expect(screen.getByRole('link', { name: /Queues/i })).toHaveAttribute('href', '/');
+ expect(screen.getByRole('link', { name: /Node Labels/i })).toHaveAttribute(
+ 'href',
+ '/node-labels',
+ );
+ expect(screen.getByRole('link', { name: /Global Settings/i })).toHaveAttribute(
+ 'href',
+ '/global-settings',
+ );
+ });
+ });
+
+ describe('Active State', () => {
+ it('should mark Queues as active when on root path', () => {
+ mockLocation.pathname = '/';
+ render();
+
+ const queuesLink = screen.getByRole('link', { name: /Queues/i });
+ const nodeLabelsLink = screen.getByRole('link', { name: /Node Labels/i });
+ const globalSettingsLink = screen.getByRole('link', { name: /Global Settings/i });
+
+ expect(queuesLink).toHaveAttribute('data-active', 'true');
+ expect(nodeLabelsLink).toHaveAttribute('data-active', 'false');
+ expect(globalSettingsLink).toHaveAttribute('data-active', 'false');
+ });
+
+ it('should mark Node Labels as active when on node labels path', () => {
+ mockLocation.pathname = '/node-labels';
+ render();
+
+ const queuesLink = screen.getByRole('link', { name: /Queues/i });
+ const nodeLabelsLink = screen.getByRole('link', { name: /Node Labels/i });
+ const globalSettingsLink = screen.getByRole('link', { name: /Global Settings/i });
+
+ expect(queuesLink).toHaveAttribute('data-active', 'false');
+ expect(nodeLabelsLink).toHaveAttribute('data-active', 'true');
+ expect(globalSettingsLink).toHaveAttribute('data-active', 'false');
+ });
+
+ it('should mark Global Settings as active when on global settings path', () => {
+ mockLocation.pathname = '/global-settings';
+ render();
+
+ const queuesLink = screen.getByRole('link', { name: /Queues/i });
+ const nodeLabelsLink = screen.getByRole('link', { name: /Node Labels/i });
+ const globalSettingsLink = screen.getByRole('link', { name: /Global Settings/i });
+
+ expect(queuesLink).toHaveAttribute('data-active', 'false');
+ expect(nodeLabelsLink).toHaveAttribute('data-active', 'false');
+ expect(globalSettingsLink).toHaveAttribute('data-active', 'true');
+ });
+
+ it('should not mark any item as active on unknown paths', () => {
+ mockLocation.pathname = '/unknown-path';
+ render();
+
+ const queuesLink = screen.getByRole('link', { name: /Queues/i });
+ const nodeLabelsLink = screen.getByRole('link', { name: /Node Labels/i });
+ const globalSettingsLink = screen.getByRole('link', { name: /Global Settings/i });
+
+ expect(queuesLink).toHaveAttribute('data-active', 'false');
+ expect(nodeLabelsLink).toHaveAttribute('data-active', 'false');
+ expect(globalSettingsLink).toHaveAttribute('data-active', 'false');
+ });
+ });
+
+ describe('Component Structure', () => {
+ it('should render with correct sidebar variant', () => {
+ render();
+
+ const sidebar = screen.getByTestId('sidebar');
+ expect(sidebar).toHaveAttribute('variant', 'inset');
+ });
+
+ it('should render sidebar components in correct hierarchy', () => {
+ render();
+
+ const sidebar = screen.getByTestId('sidebar');
+ const header = screen.getByTestId('sidebar-header');
+ const content = screen.getByTestId('sidebar-content');
+ const menu = screen.getByTestId('sidebar-menu');
+
+ // Check hierarchy
+ expect(sidebar).toContainElement(header);
+ expect(sidebar).toContainElement(content);
+ expect(content).toContainElement(menu);
+
+ // Check header comes before content
+ const allElements = sidebar.querySelectorAll('[data-testid]');
+ const headerIndex = Array.from(allElements).findIndex(
+ (el) => el.getAttribute('data-testid') === 'sidebar-header',
+ );
+ const contentIndex = Array.from(allElements).findIndex(
+ (el) => el.getAttribute('data-testid') === 'sidebar-content',
+ );
+ expect(headerIndex).toBeLessThan(contentIndex);
+ });
+ });
+
+ describe('Icons', () => {
+ it('should render icons for each navigation item', () => {
+ render();
+
+ // Check that each link contains an icon (SVG element)
+ const links = screen.getAllByRole('link');
+ links.forEach((link) => {
+ const svg = link.querySelector('svg');
+ expect(svg).toBeInTheDocument();
+ expect(svg).toHaveClass('h-4', 'w-4');
+ });
+ });
+ });
+
+ describe('Reactive Navigation', () => {
+ it('should update active state when location changes', () => {
+ const { rerender } = render();
+
+ // Initially on root
+ expect(screen.getByRole('link', { name: /Queues/i })).toHaveAttribute('data-active', 'true');
+
+ // Change location
+ mockLocation.pathname = '/node-labels';
+ rerender();
+
+ expect(screen.getByRole('link', { name: /Queues/i })).toHaveAttribute('data-active', 'false');
+ expect(screen.getByRole('link', { name: /Node Labels/i })).toHaveAttribute(
+ 'data-active',
+ 'true',
+ );
+ });
+ });
+
+ describe('Edge Cases', () => {
+ it('should handle navigation items without icons gracefully', () => {
+ // This test ensures the component doesn't crash if icon is undefined
+ // though in the current implementation all items have icons
+ render();
+
+ // Component should render without errors
+ expect(screen.getByTestId('sidebar')).toBeInTheDocument();
+ });
+
+ it('should handle empty pathname gracefully', () => {
+ mockLocation.pathname = '';
+ render();
+
+ // Should still render all navigation items
+ expect(screen.getByRole('link', { name: /Queues/i })).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: /Node Labels/i })).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: /Global Settings/i })).toBeInTheDocument();
+ });
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/layouts/app-sidebar.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/layouts/app-sidebar.tsx
new file mode 100644
index 0000000000000..6bad64fef07e2
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/layouts/app-sidebar.tsx
@@ -0,0 +1,85 @@
+/**
+ * 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 { Link, useLocation } from 'react-router';
+import { LayoutDashboard, Tag, Settings, ListFilter } from 'lucide-react';
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuItem,
+ SidebarMenuButton,
+} from '~/components/ui/sidebar';
+
+const navigation = [
+ {
+ path: '/',
+ title: 'Queues',
+ icon: LayoutDashboard,
+ },
+ {
+ path: '/global-settings',
+ title: 'Global Settings',
+ icon: Settings,
+ },
+ {
+ path: '/placement-rules',
+ title: 'Placement Rules',
+ icon: ListFilter,
+ },
+ {
+ path: '/node-labels',
+ title: 'Node Labels',
+ icon: Tag,
+ },
+];
+
+export function AppSidebar() {
+ const location = useLocation();
+
+ return (
+
+
+
+
Capacity Scheduler UI
+
+
+
+
+ {navigation.map((item) => {
+ const Icon = item.icon;
+ const isActive = location.pathname === item.path;
+
+ return (
+
+
+
+
+ {item.title}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-context.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-context.ts
new file mode 100644
index 0000000000000..1a3f0d7c79ec2
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-context.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 { createContext } from 'react';
+
+type Theme = 'dark' | 'light' | 'system';
+
+export interface ThemeProviderState {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+}
+
+export const ThemeProviderContext = createContext(undefined);
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-provider.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-provider.test.tsx
new file mode 100644
index 0000000000000..a05630c98c712
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-provider.test.tsx
@@ -0,0 +1,258 @@
+/**
+ * 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 { render, screen, renderHook, act, waitFor } from '@testing-library/react';
+import { vi } from 'vitest';
+import { ThemeProvider } from './theme-provider';
+import { useTheme } from './use-theme';
+
+// Mock localStorage
+const localStorageMock = {
+ getItem: vi.fn(),
+ setItem: vi.fn(),
+ removeItem: vi.fn(),
+ clear: vi.fn(),
+ length: 0,
+ key: vi.fn(),
+};
+Object.defineProperty(window, 'localStorage', { value: localStorageMock });
+
+// Mock matchMedia
+const matchMediaMock = vi.fn();
+Object.defineProperty(window, 'matchMedia', {
+ value: matchMediaMock,
+ writable: true,
+});
+
+describe('ThemeProvider', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Reset document classes
+ document.documentElement.className = '';
+ // Default matchMedia to light mode
+ matchMediaMock.mockReturnValue({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ });
+ });
+
+ it('should provide theme context to children', () => {
+ const TestComponent = () => {
+ const { theme } = useTheme();
+ return
;
+ };
+
+ // Suppress console.error for this test
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ expect(() => render()).toThrow('useTheme must be used within a ThemeProvider');
+
+ consoleSpy.mockRestore();
+ });
+
+ it('should use default theme when no stored value exists', () => {
+ localStorageMock.getItem.mockReturnValue(null);
+
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(result.current.theme).toBe('light');
+ });
+
+ it('should load theme from localStorage on mount', () => {
+ localStorageMock.getItem.mockReturnValue('dark');
+
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(localStorageMock.getItem).toHaveBeenCalledWith('test-theme');
+ expect(result.current.theme).toBe('dark');
+ });
+
+ it('should save theme to localStorage when changed', () => {
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ act(() => {
+ result.current.setTheme('dark');
+ });
+
+ expect(localStorageMock.setItem).toHaveBeenCalledWith('test-theme', 'dark');
+ expect(result.current.theme).toBe('dark');
+ });
+
+ it('should apply correct class to document root for light theme', () => {
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ act(() => {
+ result.current.setTheme('light');
+ });
+
+ expect(document.documentElement.classList.contains('light')).toBe(true);
+ expect(document.documentElement.classList.contains('dark')).toBe(false);
+ });
+
+ it('should apply correct class to document root for dark theme', () => {
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ act(() => {
+ result.current.setTheme('dark');
+ });
+
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
+ expect(document.documentElement.classList.contains('light')).toBe(false);
+ });
+
+ it('should apply system theme based on prefers-color-scheme when theme is system', () => {
+ // Mock system prefers dark mode
+ matchMediaMock.mockReturnValue({
+ matches: true, // dark mode
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ });
+
+ renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
+ expect(document.documentElement.classList.contains('light')).toBe(false);
+ });
+
+ it('should apply light theme when system prefers light mode', async () => {
+ // Ensure localStorage is not returning any stored value
+ localStorageMock.getItem.mockReturnValue(null);
+
+ // Reset document classes before test
+ document.documentElement.className = '';
+
+ // Mock system prefers light mode
+ matchMediaMock.mockReturnValue({
+ matches: false, // light mode
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ });
+
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ // Wait for all effects to complete
+ await waitFor(() => {
+ expect(document.documentElement.classList.contains('light')).toBe(true);
+ });
+
+ expect(document.documentElement.classList.contains('dark')).toBe(false);
+ expect(result.current.theme).toBe('system');
+ });
+
+ it('should handle localStorage errors gracefully when loading', () => {
+ localStorageMock.getItem.mockImplementation(() => {
+ throw new Error('localStorage not available');
+ });
+
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ // Should fall back to default theme
+ expect(result.current.theme).toBe('light');
+ });
+
+ it('should handle localStorage errors gracefully when saving', () => {
+ localStorageMock.setItem.mockImplementation(() => {
+ throw new Error('localStorage not available');
+ });
+
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ // Should not throw when setting theme
+ expect(() => {
+ act(() => {
+ result.current.setTheme('dark');
+ });
+ }).not.toThrow();
+
+ // Theme should still be updated in state
+ expect(result.current.theme).toBe('dark');
+ });
+
+ it('should ignore invalid stored theme values', () => {
+ localStorageMock.getItem.mockReturnValue('invalid-theme');
+
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ // Should use default theme when stored value is invalid
+ expect(result.current.theme).toBe('light');
+ });
+
+ it('should update theme classes when theme changes', () => {
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ // Start with light theme
+ act(() => {
+ result.current.setTheme('light');
+ });
+ expect(document.documentElement.classList.contains('light')).toBe(true);
+
+ // Change to dark theme
+ act(() => {
+ result.current.setTheme('dark');
+ });
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
+ expect(document.documentElement.classList.contains('light')).toBe(false);
+
+ // Change to system theme
+ act(() => {
+ result.current.setTheme('system');
+ });
+ // With default mock (light mode preference)
+ expect(document.documentElement.classList.contains('light')).toBe(true);
+ expect(document.documentElement.classList.contains('dark')).toBe(false);
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-provider.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-provider.tsx
new file mode 100644
index 0000000000000..9081c49d29900
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/theme-provider.tsx
@@ -0,0 +1,85 @@
+/**
+ * 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 { useEffect, useState } from 'react';
+import { ThemeProviderContext } from './theme-context';
+
+type Theme = 'dark' | 'light' | 'system';
+
+interface ThemeProviderProps {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+}
+
+export function ThemeProvider({
+ children,
+ defaultTheme = 'system',
+ storageKey = 'vite-ui-theme',
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(defaultTheme);
+
+ // Load theme from localStorage after mount
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ try {
+ const stored = window.localStorage.getItem(storageKey);
+ if (stored === 'dark' || stored === 'light' || stored === 'system') {
+ setTheme(stored);
+ }
+ } catch {
+ // Silently handle localStorage errors
+ }
+ }, [storageKey]);
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove('light', 'dark');
+
+ if (theme === 'system') {
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light';
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (newTheme: Theme) => {
+ setTheme(newTheme);
+
+ if (typeof window !== 'undefined') {
+ try {
+ window.localStorage.setItem(storageKey, newTheme);
+ } catch {
+ // Silently handle localStorage errors
+ }
+ }
+ },
+ };
+
+ return {children};
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/use-theme.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/use-theme.test.tsx
new file mode 100644
index 0000000000000..8f8c8e550e123
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/use-theme.test.tsx
@@ -0,0 +1,70 @@
+/**
+ * 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 { renderHook, act } from '@testing-library/react';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { useTheme } from './use-theme';
+import { ThemeProvider } from './theme-provider';
+
+const matchMediaMock = () => ({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ onchange: null,
+ media: '(prefers-color-scheme: dark)',
+});
+
+describe('useTheme', () => {
+ beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation(matchMediaMock),
+ });
+ });
+
+ beforeEach(() => {
+ document.documentElement.className = '';
+ window.localStorage.clear();
+ });
+
+ it('throws when used outside ThemeProvider', () => {
+ expect(() => renderHook(() => useTheme())).toThrowError(
+ 'useTheme must be used within a ThemeProvider',
+ );
+ });
+
+ it('provides theme value and setter inside ThemeProvider', () => {
+ const { result } = renderHook(() => useTheme(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(result.current.theme).toBe('system');
+
+ act(() => {
+ result.current.setTheme('dark');
+ });
+
+ expect(result.current.theme).toBe('dark');
+ expect(window.localStorage.getItem('test-theme')).toBe('dark');
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
+ });
+});
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/use-theme.ts b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/use-theme.ts
new file mode 100644
index 0000000000000..0cd1ee2b4ea9f
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/providers/use-theme.ts
@@ -0,0 +1,31 @@
+/**
+ * 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 { use } from 'react';
+import { ThemeProviderContext } from './theme-context';
+
+export const useTheme = () => {
+ const context = use(ThemeProviderContext);
+
+ if (context === undefined) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+
+ return context;
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/search/HighlightedText.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/search/HighlightedText.tsx
new file mode 100644
index 0000000000000..00c06fc2cf8a8
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/search/HighlightedText.tsx
@@ -0,0 +1,63 @@
+/**
+ * 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.
+ */
+
+
+/**
+ * Component that highlights search terms within text
+ */
+
+import React from 'react';
+
+interface HighlightedTextProps {
+ text: string;
+ highlight: string;
+ className?: string;
+}
+
+/**
+ * Escapes special regex characters in a string
+ */
+const escapeRegExp = (string: string): string => {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+};
+
+export const HighlightedText: React.FC = ({ text, highlight, className }) => {
+ if (!highlight || !highlight.trim()) {
+ return {text};
+ }
+
+ // Split text by the highlight term (case-insensitive)
+ const escapedHighlight = escapeRegExp(highlight.trim());
+ const parts = text.split(new RegExp(`(${escapedHighlight})`, 'gi'));
+
+ return (
+
+ {/* eslint-disable @eslint-react/no-array-index-key */}
+ {parts.map((part, i) =>
+ part.toLowerCase() === highlight.toLowerCase() ? (
+
+ {part}
+
+ ) : (
+ {part}
+ ),
+ )}
+ {/* eslint-enable @eslint-react/no-array-index-key */}
+
+ );
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/search/NodeLabelSelector.test.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/search/NodeLabelSelector.test.tsx
new file mode 100644
index 0000000000000..1b316967014f1
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/search/NodeLabelSelector.test.tsx
@@ -0,0 +1,156 @@
+/**
+ * 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, vi, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { NodeLabelSelector } from './NodeLabelSelector';
+import { useSchedulerStore } from '~/stores/schedulerStore';
+import type { NodeLabel } from '~/types';
+
+// Mock the scheduler store
+vi.mock('~/stores/schedulerStore');
+
+// Mock Radix UI Select to avoid pointer capture issues
+vi.mock('~/components/ui/select', () => ({
+ Select: ({ children, value, onValueChange }: any) => (
+
+ );
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/accordion.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/accordion.tsx
new file mode 100644
index 0000000000000..5215a8fba2803
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/accordion.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 * as React from 'react';
+import * as AccordionPrimitive from '@radix-ui/react-accordion';
+import { ChevronDownIcon } from 'lucide-react';
+
+import { cn } from '~/utils/cn';
+
+function Accordion({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180',
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ );
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
{children}
+
+ );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/alert.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/alert.tsx
new file mode 100644
index 0000000000000..4e709ee15ec1f
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/alert.tsx
@@ -0,0 +1,80 @@
+/**
+ * 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 * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '~/utils/cn';
+
+const alertVariants = cva(
+ 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
+ {
+ variants: {
+ variant: {
+ default: 'bg-card text-card-foreground',
+ destructive:
+ 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
+ secondary: 'bg-secondary text-secondary-foreground border-transparent',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ );
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/badge.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/badge.tsx
new file mode 100644
index 0000000000000..64d7f4bd93103
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/badge.tsx
@@ -0,0 +1,60 @@
+/**
+ * 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 * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '~/utils/cn';
+
+const badgeVariants = cva(
+ 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
+ {
+ variants: {
+ variant: {
+ default: 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
+ destructive:
+ 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
+ success: 'border-transparent bg-queue-running text-white [a&]:hover:bg-queue-running/90',
+ warning: 'border-transparent bg-queue-draining text-white [a&]:hover:bg-queue-draining/90',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'span'> & VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'span';
+
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/button.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/button.tsx
new file mode 100644
index 0000000000000..b4686ec9e639a
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/button.tsx
@@ -0,0 +1,75 @@
+/**
+ * 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 * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '~/utils/cn';
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline:
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
+ secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot : 'button';
+
+ return (
+
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/card.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/card.tsx
new file mode 100644
index 0000000000000..6ae89a7702263
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/card.tsx
@@ -0,0 +1,94 @@
+/**
+ * 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 * as React from 'react';
+
+import { cn } from '~/utils/cn';
+
+function Card({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
+ return ;
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/checkbox.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000000000..52d80d2bd7aaf
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/checkbox.tsx
@@ -0,0 +1,46 @@
+/**
+ * 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 * as React from 'react';
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import { CheckIcon } from 'lucide-react';
+
+import { cn } from '~/utils/cn';
+
+function Checkbox({ className, ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+
+ );
+}
+
+export { Checkbox };
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/collapsible.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000000000..d3ab7da54b050
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/collapsible.tsx
@@ -0,0 +1,38 @@
+/**
+ * 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 * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
+
+function Collapsible({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/combobox.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/combobox.tsx
new file mode 100644
index 0000000000000..96f45c12b27bc
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/combobox.tsx
@@ -0,0 +1,135 @@
+/**
+ * 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.
+ */
+
+
+'use client';
+
+import * as React from 'react';
+import { Check, ChevronDown } from 'lucide-react';
+
+import { cn } from '~/utils/cn';
+import { Button } from '~/components/ui/button';
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '~/components/ui/command';
+import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover';
+
+export interface ComboboxItem {
+ value: string;
+ label: string;
+}
+
+interface ComboboxProps {
+ value?: string;
+ onValueChange?: (value: string) => void;
+ items: ComboboxItem[];
+ placeholder?: string;
+ searchPlaceholder?: string;
+ emptyText?: string;
+ className?: string;
+ 'aria-label'?: string;
+}
+
+export function Combobox({
+ value,
+ onValueChange,
+ items,
+ placeholder = 'Select an option...',
+ searchPlaceholder = 'Search...',
+ emptyText = 'No results found.',
+ className,
+ 'aria-label': ariaLabel,
+}: ComboboxProps) {
+ const [open, setOpen] = React.useState(false);
+ const triggerRef = React.useRef(null);
+ const [triggerWidth, setTriggerWidth] = React.useState();
+
+ React.useEffect(() => {
+ if (triggerRef.current) {
+ setTriggerWidth(triggerRef.current.offsetWidth);
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+ {emptyText}
+
+ {items.map((item) => (
+ {
+ onValueChange?.(currentValue === value ? '' : currentValue);
+ setOpen(false);
+ }}
+ >
+
+ {item.label}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/command.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/command.tsx
new file mode 100644
index 0000000000000..02ed9378eb13f
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/command.tsx
@@ -0,0 +1,178 @@
+/**
+ * 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 * as React from 'react';
+import { Command as CommandPrimitive } from 'cmdk';
+import { SearchIcon } from 'lucide-react';
+
+import { cn } from '~/utils/cn';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '~/components/ui/dialog';
+
+function Command({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandDialog({
+ title = 'Command Palette',
+ description = 'Search for a command to run...',
+ children,
+ className,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ title?: string;
+ description?: string;
+ className?: string;
+ showCloseButton?: boolean;
+}) {
+ return (
+
+ );
+}
+
+function CommandInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ );
+}
+
+function CommandList({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandEmpty({ ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandItem({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ );
+}
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/context-menu.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/context-menu.tsx
new file mode 100644
index 0000000000000..2bcd99583bd71
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/context-menu.tsx
@@ -0,0 +1,241 @@
+/**
+ * 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 * as React from 'react';
+import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
+import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
+
+import { cn } from '~/utils/cn';
+
+function ContextMenu({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function ContextMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function ContextMenuGroup({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function ContextMenuPortal({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function ContextMenuSub({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function ContextMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function ContextMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean;
+}) {
+ return (
+
+ {children}
+
+
+ );
+}
+
+function ContextMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function ContextMenuContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function ContextMenuItem({
+ className,
+ inset,
+ variant = 'default',
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean;
+ variant?: 'default' | 'destructive';
+}) {
+ return (
+
+ );
+}
+
+function ContextMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+function ContextMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+function ContextMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean;
+}) {
+ return (
+
+ );
+}
+
+function ContextMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ );
+}
+
+export {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup,
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/dialog.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/dialog.tsx
new file mode 100644
index 0000000000000..cac45ca1069e3
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/dialog.tsx
@@ -0,0 +1,148 @@
+/**
+ * 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.
+ */
+
+
+'use client';
+
+import * as React from 'react';
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import { XIcon } from 'lucide-react';
+
+import { cn } from '~/utils/cn';
+
+function Dialog({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function DialogTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function DialogPortal({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function DialogClose({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean;
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ );
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function DialogTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/drawer.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/drawer.tsx
new file mode 100644
index 0000000000000..bf587543fd565
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/drawer.tsx
@@ -0,0 +1,118 @@
+/**
+ * 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 * as React from 'react';
+import { Drawer as DrawerPrimitive } from 'vaul';
+
+import { cn } from '~/utils/cn';
+
+const Drawer = ({
+ shouldScaleBackground = true,
+ ...props
+}: React.ComponentProps) => (
+
+);
+Drawer.displayName = 'Drawer';
+
+const DrawerTrigger = DrawerPrimitive.Trigger;
+
+const DrawerPortal = DrawerPrimitive.Portal;
+
+const DrawerClose = DrawerPrimitive.Close;
+
+const DrawerOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
+
+const DrawerContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+));
+DrawerContent.displayName = 'DrawerContent';
+
+const DrawerHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+DrawerHeader.displayName = 'DrawerHeader';
+
+const DrawerFooter = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+DrawerFooter.displayName = 'DrawerFooter';
+
+const DrawerTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
+
+const DrawerDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/dropdown-menu.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000000000..461a3ddeb1948
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,247 @@
+/**
+ * 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.
+ */
+
+
+'use client';
+
+import * as React from 'react';
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
+
+import { cn } from '~/utils/cn';
+
+function DropdownMenu({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function DropdownMenuGroup({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = 'default',
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean;
+ variant?: 'default' | 'destructive';
+}) {
+ return (
+
+ );
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean;
+}) {
+ return (
+
+ );
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ );
+}
+
+function DropdownMenuSub({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean;
+}) {
+ return (
+
+ {children}
+
+
+ );
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/field-select.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/field-select.tsx
new file mode 100644
index 0000000000000..ae9aa9434128b
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/field-select.tsx
@@ -0,0 +1,128 @@
+/**
+ * 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 React from 'react';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '~/components/ui/select';
+import {
+ Field,
+ FieldControl,
+ FieldDescription,
+ FieldLabel,
+ FieldMessage,
+ type FieldProps,
+} from '~/components/ui/field';
+import { cn } from '~/utils/cn';
+
+type SelectBaseProps = React.ComponentProps;
+type FieldLabelProps = React.ComponentPropsWithoutRef;
+type FieldDescriptionProps = React.ComponentPropsWithoutRef;
+type FieldMessageProps = React.ComponentPropsWithoutRef;
+
+export interface SelectOption {
+ value: string;
+ label: string;
+ description?: string;
+}
+
+interface FieldSelectProps extends Omit {
+ label?: React.ReactNode;
+ description?: React.ReactNode;
+ message?: React.ReactNode;
+ options: SelectOption[];
+ id?: string;
+ fieldName?: FieldProps['name'];
+ fieldClassName?: string;
+ selectClassName?: string;
+ triggerClassName?: string;
+ labelProps?: FieldLabelProps;
+ descriptionProps?: FieldDescriptionProps;
+ messageProps?: FieldMessageProps;
+ placeholder?: string;
+ disabled?: boolean;
+}
+
+export const FieldSelect: React.FC = ({
+ label,
+ description,
+ message,
+ options,
+ id,
+ fieldName,
+ fieldClassName,
+ selectClassName,
+ triggerClassName,
+ labelProps,
+ descriptionProps,
+ messageProps,
+ placeholder,
+ disabled,
+ ...selectProps
+}) => {
+ return (
+
+ {label ? (
+
+ {label}
+
+ ) : null}
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+ {message ? (
+
+ {message}
+
+ ) : null}
+
+ );
+};
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/field-switch.tsx b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/field-switch.tsx
new file mode 100644
index 0000000000000..9a7497da499ca
--- /dev/null
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-capacity-scheduler-ui/src/main/webapp/src/components/ui/field-switch.tsx
@@ -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 React from 'react';
+import { Switch } from '~/components/ui/switch';
+import {
+ Field,
+ FieldControl,
+ FieldDescription,
+ FieldLabel,
+ FieldMessage,
+ type FieldProps,
+} from '~/components/ui/field';
+import { cn } from '~/utils/cn';
+
+type SwitchBaseProps = React.ComponentProps;
+type FieldLabelProps = React.ComponentPropsWithoutRef;
+type FieldDescriptionProps = React.ComponentPropsWithoutRef;
+type FieldMessageProps = React.ComponentPropsWithoutRef;
+
+interface FieldSwitchProps extends Omit {
+ label?: React.ReactNode;
+ labelSuffix?: React.ReactNode;
+ description?: React.ReactNode;
+ message?: React.ReactNode;
+ addon?: React.ReactNode;
+ id?: string;
+ fieldName?: FieldProps['name'];
+ fieldClassName?: string;
+ wrapperClassName?: string;
+ switchWrapperClassName?: string;
+ controlClassName?: string;
+ switchClassName?: string;
+ labelProps?: FieldLabelProps;
+ descriptionProps?: FieldDescriptionProps;
+ messageProps?: FieldMessageProps;
+ align?: 'start' | 'center';
+}
+
+export const FieldSwitch: React.FC = ({
+ label,
+ labelSuffix,
+ description,
+ addon,
+ message,
+ id,
+ fieldName,
+ fieldClassName,
+ wrapperClassName,
+ switchWrapperClassName,
+ controlClassName,
+ switchClassName,
+ labelProps,
+ descriptionProps,
+ messageProps,
+ align = 'center',
+ ...switchProps
+}) => {
+ return (
+
+