Преглед изворни кода

[ui-core] add cursor rules and improve code examples

Björn Alm пре 3 месеци
родитељ
комит
8043281f8d

+ 12 - 0
.cursor/rules/accessibility.mdc

@@ -0,0 +1,12 @@
+---
+description: Accessibility rules for components, styles, and templates.
+globs: ["**/*.tsx", "**/templates/**", "**/*.scss"]
+alwaysApply: false
+---
+
+# A11y Essentials
+- Use semantic elements; controls must have accessible names.
+- Dialogs: trap focus, restore on close, label/describe properly.
+- Announce async states (loading, error, empty); no color-only meanings.
+- Use ARIA when elements might not fully describe a complex widget's purpose or behavior
+

+ 15 - 0
.cursor/rules/build-and-scripts.mdc

@@ -0,0 +1,15 @@
+---
+description: Build/dev scripts norms; attach to package/build/config files.
+globs: ["package.json", "webpack*.js", "tsconfig.json", "Makefile", "pyproject.toml"]
+alwaysApply: false
+---
+
+# Build/Dev
+- Frontend dev: `npm run dev`, `dev-login`, `dev-workers`.
+- Production bundles: `npm run webpack*`.
+- Python apps: `make apps` / `make desktop`.
+- CI: scripts must be non-interactive and idempotent.
+
+## Checklists
+- Register new React roots in relevant imports/bootstrap as required by Hue.
+- Keep TS strict; no downlevel that breaks antd/cuix typings.

+ 11 - 0
.cursor/rules/code-quality-and-lint.mdc

@@ -0,0 +1,11 @@
+---
+description: ESLint/Prettier/Stylelint/TS strictness; attach broadly to source and configs.
+globs: [".eslintrc.*", ".prettierrc*", ".stylelintrc*", "**/*.ts", "**/*.tsx", "**/*.scss"]
+alwaysApply: false
+---
+
+# Code Quality
+- Run `npm run lint`, `style-lint`, `prettier` before committing.
+- Do not disable rules without justification in-code.
+- Prefer `readonly`, narrow `unknown`, avoid `any`.
+- Keep imports ordered; avoid unused exports.

+ 32 - 0
.cursor/rules/frontend-conventions.mdc

@@ -0,0 +1,32 @@
+---
+description: Conventions for React/TS, imports, naming, structure; auto-attach to UI code.
+globs: ["**/*.ts", "**/*.tsx"]
+alwaysApply: false
+---
+
+# React + TS Conventions
+- Use `interface` for props; no `any`; narrow `unknown`.
+- Import order: externals → internal utils/hooks → types → styles (last).
+- Keep components cohesive; lift state; split large parts into small pure pieces.
+
+## New react components 
+- Component folder: `ComponentName/ComponentName.tsx` + `ComponentName.test.tsx` + `ComponentName.scss`.
+- Do not create an index.ts file 
+- Always the create a unit test and if needed the scss file
+- Always include the Cloudera copyright msg at the beginning of the file
+- Global and reusable components are placed in folder `desktop/core/src/desktop/js/reactComponents`
+- Put app specific components under that app, e.g. `desktop/core/src/desktop/js/apps/editor/components/`
+
+## Example Skeleton
+```ts
+export interface FooProps { title: string; onAction?: () => void }
+export const Foo = ({ title, onAction }: FooProps) => (
+  <div className="foo">
+    <h2>{title}</h2>
+    <button onClick={onAction}>{t('Click me')}</button>
+  </div>
+);
+```
+
+## Performance
+Lazy-load heavy chunks; debounce/cancel network work; memoize expensive calcs.

+ 21 - 0
.cursor/rules/i18n-and-copy.mdc

@@ -0,0 +1,21 @@
+---
+description: i18n and microcopy rules for all user-facing text.
+globs: ["**/*.tsx", "**/*.ts", "**/*.vue", "**/*.ko", "**/templates/**"]
+alwaysApply: false
+---
+
+# i18n & Copy
+- All user-facing strings go through i18n (`useTranslation()` in `i18nReact`).
+- Copy should be concise, action-oriented, and consistent.
+- Error messages = clear cause + actionable next step.
+- Default namespace `translation` unless justified otherwise.
+
+
+## Example
+```ts
+import { i18nReact } from '../../../utils/i18nReact';
+
+const { t } = i18nReact.useTranslation();
+
+const buttonLabel = t('click me')
+```t

+ 15 - 0
.cursor/rules/integration-bridges.mdc

@@ -0,0 +1,15 @@
+---
+description: Guidance for React mounting in templates and KO/Vue boundaries.
+globs: ["**/templates/**", "**/*.ko", "**/*.vue", "**/*.tsx"]
+alwaysApply: false
+---
+
+# Integration Patterns
+- **KO → React**: use `reactWrapper` binding; keep KO observables at boundary.
+- **Templates (Mako/HTML)**: mount via `data-reactcomponent` on stable containers.
+- **Vue legacy**: do not introduce new Vue; prefer React for new work.
+
+## Pitfalls
+- Ensure cuix/antd CSS scope present at React root.
+- Avoid dual ownership of DOM by different frameworks.
+

+ 11 - 0
.cursor/rules/legacy-migration.mdc

@@ -0,0 +1,11 @@
+---
+description: Strategy for migrating jQuery/KO/Less pieces toward React/SCSS.
+globs: ["**/jquery/**", "**/*.less", "**/*.ko", "**/*.vue", "**/*.tsx"]
+alwaysApply: false
+---
+
+# Migration Strategy
+- When touching legacy code, always ask before adding typed adapters and moving logic to React/TS.
+- Replace jQuery DOM ops with state/effects; keep remaining jQuery isolated.
+- Only convert Less→SCSS for code that is migrated to react.js, don't mix less and scss for the same component.
+- Add regression tests before refactors.

+ 41 - 0
.cursor/rules/pr-review-checklist.mdc

@@ -0,0 +1,41 @@
+---
+description: PR review output policy and checklist;
+alwaysApply: false
+---
+# PR Review 
+
+## Instructions
+— Only Output Issues/Improvements, DO NOT list all that is already good.
+- Be concise. 
+- Provide a general feedback section for issues spanning multiple files, e.g. architectural.
+- Provide a section per file with links to the source files.
+
+## Always check for
+- Architecture: lift/split state; keep components cohesive.
+- Naming clarity: components, functions, variables.
+- Edge cases: loading/empty/error; network failures.
+- Accessibility: roles, labels, focus, contrast. Only comment where needed, 
+- Check for aria- attributes ONLY when needed. e.g. 
+  - DO: comment if an icon inly button is missing aria-label. 
+  - DON'T: comment about missing aria-disabled if a native disabled attribute is used.
+- Copy: concise, consistent; i18n everywhere.
+- All files must start with the Cloudera copyright statement
+
+## Make sure that test files
+- test main flows
+- use userEvent and avoid fireEvent unless realy needed.
+- does not test implementation details; assert on behavior and rendered screens.
+- use role/name queries, e.g. getByRole rather than getByText
+- uses toBeVisible() instead of toBeInTheDocument() where possible
+- use the component name in the describe function
+- use "it" function over "test" for better readbility
+
+
+## Make sure the style files
+- use BEM notation (avoid unnecessary nesting for classes but allow .antd.cuix on the root)
+- never use hardcoded colors; use tokens/variables.
+- avoid hardcoded variables for positioning/spacing if possible
+- do not contain unnused classes
+- use classes over HTML elements whenever possible
+
+ 

+ 68 - 0
.cursor/rules/project-overview.mdc

@@ -0,0 +1,68 @@
+---
+description: Terse Hue overview + global constraints; always include in all requests.
+globs:
+alwaysApply: true
+---
+
+# Hue Overview (short)
+- Backend: Python/Django; Frontend: React (primary) + some Vue/KO/jQuery.
+- Styling: SCSS (BEM). Avoid adding new Less; migrate to SCSS when touching.
+- UI libs: Ant Design (antd) + Cloudera UI (cuix).
+- TS strict; avoid `any`. Prefer named type imports.
+- No CSS Modules; use global SCSS conventions.
+
+## Golden Rules
+- Focus on the task given, do not expand your tasks without requesting confirmation first.
+- Do not automatically start coding if you are asked a question. Only code when asked to.
+- Always try to follow best practices and avoid hacks
+- Write Clean Code with Meaningful names, Single responsibility, Expressive code etc
+- Prefer **React + TypeScript** for new UI.
+- Maintain KO↔React via `reactWrapper` where used.
+- No hardcoded color codes; use tokens/variables.
+- All user-facing text must be i18n'd.
+
+
+## Project Structure
+hue/
+├── apps/                          # Backend Django applications
+│   ├── beeswax/                   # Hive query interface
+│   ├── filebrowser/               # File system browser
+│   ├── impala/                    # Impala query interface
+│   ├── jobbrowser/                # Job monitoring
+│   ├── metastore/                 # Metadata management
+│   ├── notebook/                  # Interactive notebooks
+│   ├── oozie/                     # Workflow management
+│   ├── rdbms/                     # RDBMS connectors
+│   └── ...                        # Other backend apps
+├── desktop/                       # Core desktop functionality
+│   ├── core/                      # Core desktop components
+│   │   └── src/desktop/
+│   │       ├── js/                # Frontend JavaScript/TypeScript
+│   │       │   ├── apps/          # Application-specific components
+│   │       │   │   ├── editor/    # SQL Editor app
+│   │       │   │   ├── notebook/  # Notebook app
+│   │       │   │   ├── tableBrowser/
+│   │       │   │   └── jobBrowser/
+│   │       │   ├── reactComponents/ # Shared React components
+│   │       │   ├── vue/           # Vue.js components
+│   │       │   ├── ko/            # Knockout.js components
+│   │       │   ├── components/    # Legacy components
+│   │       │   ├── utils/         # Utility functions
+│   │       │   ├── api/           # API utilities
+│   │       │   └── types/         # TypeScript type definitions
+│   │       ├── static/            # Static assets
+│   │       └── templates/         # Django templates
+│   └── libs/                      # Shared libraries
+├── tools/                         # Build and development tools
+├── docs/                          # Documentation
+├── ext/                           # External dependencies
+├── maven/                         # Maven build configuration
+├── package.json                   # Frontend dependencies
+├── pyproject.toml                 # Python project configuration
+├── webpack.config.js              # Webpack configuration
+├── tsconfig.json                  # TypeScript configuration
+├── jest.config.js                 # Jest testing configuration
+├── .eslintrc.js                   # ESLint configuration
+├── .prettierrc                    # Prettier configuration
+├── .stylelintrc                   # Stylelint configuration
+└── Makefile                       # Build automation

+ 21 - 0
.cursor/rules/styling-and-design-tokens.mdc

@@ -0,0 +1,21 @@
+---
+description: SCSS + BEM rules; forbid hardcoded colors; attach to style files.
+globs: ["**/*.scss", "**/*.less"]
+alwaysApply: false
+---
+
+# Styling Rules
+- **Primary:** SCSS with BEM; avoid adding new Less. Do not automigrate when touching unless you can easily migrate the entire file.
+- **No hardcoded colors**; use SCSS variables/tokens (cuix/antd) from 
+- Co-locate `ComponentName.scss` with the component; keep selectors BEM-scoped.
+- Prefer BEM modifiers over state classes
+- Prefer classes over using HTML element selectors
+
+## Example
+```scss
+@use 'variables' as vars;
+.component-name {
+  &__title {  color: vars.$hue-primary-color-dark;; }
+  &--disabled { opacity: .5; cursor: not-allowed; }
+}
+```

+ 40 - 0
.cursor/rules/testing-rtl.mdc

@@ -0,0 +1,40 @@
+---
+description: Frontend testing with React Testing Library best practices.
+globs: ["**/*.test.ts", "**/*.test.tsx", "jest.config.*"]
+alwaysApply: false
+---
+
+# Testing Rules
+- Use **React Testing Library**; prefer `user-event` over `fireEvent`.
+- Test primary flows, edge cases, and a11y roles/labels.
+- Avoid implementation details; assert on behavior and DOM.
+- prefer role/name queries, e.g. getByRole; avoid brittle hardcoded strings if translated.
+- Use the component name in the describe and use "it" over "test" for better readbility. 
+
+## Example
+```ts
+describe('MyComponent', () => {
+  it('renders calls action when clicked', () => {
+    const user = userEvent.setup();
+    render(<Foo title="Hi" />);
+    await user.click(screen.getByRole('button', { name: /click/i }));
+    expect(onAction).toHaveBeenCalled();
+  })
+})
+```
+
+# Executing tests
+```shell
+# Run all tests
+npm run test
+npm run test-coverage
+npm run test-clearCache
+# Run a single file
+npm test -- --testPathPattern=WelcomeTour
+# Run tests with verbose output
+npm test -- --verbose
+# Run tests and show console output
+npm test -- --verbose --silent=false
+# Run tests and stop on first failure
+npm test -- --bail
+```

+ 51 - 10
desktop/core/src/desktop/js/apps/editor/components/result/reactExample/ReactExample.scss

@@ -1,16 +1,57 @@
-// Component styling using BEM notation
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 'classes';
 @use 'variables' as vars;
 @use 'mixins';
 
-$my-color: #f8f8f8;
+.antd.cuix {
+  .react-example {
+    &__title,
+    &__description {
+      color: vars.$hue-primary-color-dark;
+      @include mixins.fade-in();
+    }
+
+    &__history-section {
+      margin-top: vars.$fluidx-spacing-m;
+      padding: vars.$fluidx-spacing-m;
+      border: 1px solid vars.$hue-border-color;
+      border-radius: vars.$border-radius-base;
+      background-color: vars.$fluidx-gray-100;
+    }
+
+    &__history-section-title {
+      margin-top: 0;
+      margin-bottom: vars.$fluidx-spacing-m;
+      color: vars.$hue-primary-color-dark;
+    }
+    
+    &__history-item {
+      padding: vars.$fluidx-spacing-s 0;
+      border-bottom: 1px solid vars.$hue-border-color;
+      font-size: vars.$font-size-sm;
 
-.react-example {
-  background-color: $my-color;
-}
+      &:last-child {
+        border-bottom: none;
+      }
+    }
 
-.react-example__title,
-.react-example__description {
-  color: vars.$hue-primary-color-dark;
-  @include mixins.fade-in();
-}
+    &__history-item-status {
+      color: vars.$hue-primary-color-dark;
+    }
+  }
+}

+ 60 - 3
desktop/core/src/desktop/js/apps/editor/components/result/reactExample/ReactExample.test.tsx

@@ -1,7 +1,27 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 userEvent from '@testing-library/user-event';
 import '@testing-library/jest-dom';
 
+// Mock useLoadData hook to prevent actual API calls
+jest.mock('../../../../../utils/hooks/useLoadData/useLoadData');
+
 import ReactExample from './ReactExample';
 
 // Required by the antd Pagination component
@@ -15,10 +35,47 @@ window.matchMedia = jest.fn().mockImplementation(query => {
   };
 });
 
+// NOTE! This is an example file and will not test all the code.
+// It is only used to show how to test a component.
 describe('ReactExample', () => {
-  test('shows a title', () => {
+  it('opens modal when button is clicked', async () => {
+    const user = userEvent.setup();
+    render(<ReactExample />);
+
+    await user.click(screen.getByRole('button', { name: 'Open Modal' }));
+
+    // Test that modal is open and visible by checking for modal dialog role
+    const modal = screen.getByRole('dialog');
+    expect(modal).toBeVisible();
+
+    // Test that modal can be closed
+    await user.click(screen.getByRole('button', { name: 'OK' }));
+    expect(screen.queryByRole('dialog')).toBeNull();
+  });
+
+  it('shows a title', () => {
     render(<ReactExample title="test title" />);
-    const title = screen.getByText('test title');
-    expect(title).toBeDefined();
+    const title = screen.getByRole('heading', { name: 'test title' });
+    expect(title).toBeVisible();
+  });
+
+  it('shows default title when no title prop provided', () => {
+    render(<ReactExample />);
+    // The default title comes from i18n, so we check for the component structure
+    expect(screen.getByRole('heading', { level: 1 })).toBeVisible();
+  });
+
+  it('renders primary button', () => {
+    render(<ReactExample />);
+    expect(screen.getByRole('button', { name: 'Open Modal' })).toBeVisible();
+  });
+
+  it('renders history section', () => {
+    render(<ReactExample />);
+    expect(
+      screen.getByRole('heading', {
+        name: 'Editor History (useLoadData + LoadingErrorWrapper example)'
+      })
+    ).toBeVisible();
   });
 });

+ 147 - 90
desktop/core/src/desktop/js/apps/editor/components/result/reactExample/ReactExample.tsx

@@ -1,123 +1,180 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 strict';
 
-import React, { FunctionComponent, useState } from 'react';
-import { Button, Modal, Skeleton, Pagination, Dropdown } from 'antd';
+import React, { useState } from 'react';
+import { Modal, Skeleton } from 'antd';
 import PrimaryButton from 'cuix/dist/components/Button/PrimaryButton';
+import Button from 'cuix/dist/components/Button/Button';
 import PlusCircleIcon from '@cloudera/cuix-core/icons/react/PlusCircleIcon';
 
-// Provides a i18n translation hook
-import { i18nReact } from '../../../../../utils/i18nReact';
-
-// tsx-files don't have the same baseUrl as other js files so
-// we are using a relative path when importing
 import { Ace } from '../../../../../ext/ace';
+import { i18nReact } from '../../../../../utils/i18nReact';
+import { useHuePubSub } from '../../../../../utils/hooks/useHuePubSub/useHuePubSub';
+import useLoadData from '../../../../../utils/hooks/useLoadData/useLoadData';
+import ReactExampleGlobal from '../../../../../reactComponents/ReactExampleGlobal/ReactExampleGlobal';
+import LoadingErrorWrapper from '../../../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
+import huePubSub from '../../../../../utils/huePubSub';
+import { GLOBAL_INFO_TOPIC } from '../../../../../reactComponents/GlobalAlert/events';
+import { HueAlert } from '../../../../../reactComponents/GlobalAlert/types';
 
 import { CURSOR_POSITION_CHANGED_EVENT } from '../../aceEditor/AceLocationHandler';
-import ReactExampleGlobal from '../../../../../reactComponents/ReactExampleGlobal/ReactExampleGlobal';
-import { useHuePubSub } from '../../../../../utils/hooks/useHuePubSub/useHuePubSub';
 import SqlExecutable from '../../../execution/sqlExecutable';
 
 import './ReactExample.scss';
 
 export interface ReactExampleProps {
   title?: string;
-  // This example component recieves the "activeExecutable" used in the result page but
-  // the props in general can of course be of any type
   activeExecutable?: SqlExecutable;
 }
 
-// When we have type definitions and Ace imported using webackpack we should
-// use those types instead of creating our own, e.g. Ace.Position
-interface EditorCursor {
-  position: Ace.Position;
+interface EditorHistoryEntry {
+  id: string;
+  statement: string;
+  status: string;
+  created: string;
 }
 
-// Using the FunctionComponent generic is optional. Alternatively you can explicitly
-// define the children prop like in the ReactExampleGlobal component.
-const ReactExample: FunctionComponent<ReactExampleProps> = ({ activeExecutable, ...i18n }) => {
-  // We use the translation hook WITHOUT the built in suspence, meaning that our
-  // component should handle the rendering of the loading state. This gives us the
-  // possibilty to render a loader or placeholder while waiting.
-  const { t, ready: i18nReady } = i18nReact.useTranslation(undefined, { useSuspense: false });
-
-  // This is one way we can do i18n on default values for strings passed as props.
-  // Use the prop value if present, if not we use a default value/key passed through i18n
-  const { title = t('Schedule') } = i18n;
+interface EditorHistoryResponse {
+  history: EditorHistoryEntry[];
+  total: number;
+}
 
-  // Example of having the react component rerender based on changes from useHuePubSub.
-  // Use with caution and preferrably only at the top level component in your component tree.
-  const editorCursor = useHuePubSub<EditorCursor>({ topic: CURSOR_POSITION_CHANGED_EVENT });
+/**
+ * @remarks
+ * This code is not intended for production. It only serves as an example
+ * for new developers and AI assistants.
+ * @example
+ * Usage in mako template:
+ * ```tsx
+ * <ReactExample data-bind="reactWrapper: 'ReactExample', props: { activeExecutable: activeExecutable }"></ReactExample>
+ * ```
+ */
+const ReactExample = ({ title, activeExecutable }: ReactExampleProps): JSX.Element => {
+  const { t, ready: i18nReady } = i18nReact.useTranslation(undefined, { useSuspense: false });
 
+  const displayTitle = title || t('Schedule');
+
+  // Example: Reactive data with useHuePubSub
+  const editorCursor = useHuePubSub<{ position: Ace.Position }>({
+    topic: CURSOR_POSITION_CHANGED_EVENT
+  });
+
+  // Example: API data loading with useLoadData + LoadingErrorWrapper
+  const {
+    data: historyData,
+    loading: loadingHistory,
+    error: historyError,
+    reloadData: reloadHistory
+  } = useLoadData<EditorHistoryResponse>('/api/v1/editor/get_history', {
+    params: { limit: 5 },
+    onSuccess: data => {
+      huePubSub.publish<HueAlert>(GLOBAL_INFO_TOPIC, {
+        message: t('Editor history loaded successfully with {{total}} items', { total: data.total })
+      });
+    },
+    onError: () => {} // Errors handled by LoadingErrorWrapper
+  });
+
+  // Example: getting the id of the active executable via Knockout binding
   const id = activeExecutable?.id;
-  const position =
-    editorCursor?.position !== undefined
-      ? JSON.stringify(editorCursor.position)
-      : t('Not available');
-
-  const [isModalOpen, setIsModalOpen] = useState(false);
 
-  const showModal = () => {
-    setIsModalOpen(true);
-  };
+  // Example: getting the cursor position of the editor via useHuePubSub
+  const position = editorCursor?.position
+    ? JSON.stringify(editorCursor.position)
+    : t('Not available');
 
-  const handleOk = () => {
-    setIsModalOpen(false);
-  };
-
-  const handleCancel = () => {
-    setIsModalOpen(false);
-  };
+  const [isModalOpen, setIsModalOpen] = useState(false);
 
-  const items = [
-    { label: 'item 1', key: 'item-1' }, // remember to pass the key prop
-    { label: 'item 2', key: 'item-2' }
+  const showModal = (): void => setIsModalOpen(true);
+  const handleOk = (): void => setIsModalOpen(false);
+  const handleCancel = (): void => setIsModalOpen(false);
+
+  // Example: LoadingErrorWrapper error configuration
+  const errorConfig = [
+    {
+      enabled: !!historyError,
+      message: t('Failed to load editor history'),
+      description: historyError,
+      actionText: t('Retry'),
+      onClick: reloadHistory
+    }
   ];
 
-  return !i18nReady ? (
-    <div className="antd">
-      <Skeleton />
-    </div>
-  ) : (
+  // Example: The i18n is not ready yet, so we return a skeleton
+  if (!i18nReady) {
+    return (
+      <div className="antd">
+        <Skeleton />
+      </div>
+    );
+  }
+
+  return (
     // The 'antd' class is added to the root element since we want it to apply the correct
     // "global" styling to its antd sub components, e.g. the antd Button.
-    <React.StrictMode>
-      <div className="react-example cuix antd">
-        <h1 className="react-example__title hue-h1 ">{title}</h1>
-        <Pagination
-          showTotal={total => `Total ${total} items`}
-          showSizeChanger
-          defaultCurrent={1}
-          total={500}
-        />
-        <PrimaryButton icon={<PlusCircleIcon />} onClick={showModal}>
-          {t('Open (cuix button)')}
-        </PrimaryButton>
-        <Dropdown menu={{ items }}>
-          <a href="http://www.cloudera.com">Hover me</a>
-        </Dropdown>
-
-        <p className="react-example__description">
-          I'm an Editor specific react component containing subcomponents. The dynamic id that I'm
-          getting from a Knockout observable is {id}.
-        </p>
-        <p className="react-example__description">
-          {`I'm also geting a cursor position from hue huePubSub using the hook useHuePubSub which is
-        updated on each 'editor.cursor.position.changed'. Cursor position is
-        ${position}`}
-        </p>
-        <ReactExampleGlobal className="react-example__react-example-global-component">
-          I'm a button from the application global component set
-        </ReactExampleGlobal>
-        <Button type="primary" onClick={() => console.info('clicked')}>
-          I'm an Antd button
-        </Button>
-
-        {/* The title and content of the Modal is internationlized using the t-function */}
-        <Modal title={t('Modify')} open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
-          <p>{t('Settings')}.</p>
-        </Modal>
+    // Only add the antd and cuix if this is a true react root component, i.e. has no parent react component.
+    <div className="react-example cuix antd">
+      <h1 className="react-example__title hue-h1">{displayTitle}</h1>
+      <p className="react-example__description">
+        {t(
+          "I'm an Editor specific react component containing subcomponents. The dynamic id that I'm getting from a Knockout observable is {{id}}.",
+          { id }
+        )}
+      </p>
+      <p className="react-example__description">
+        {t(
+          "I'm also getting a cursor position from hue huePubSub using the hook useHuePubSub which is updated on each 'editor.cursor.position.changed'. Cursor position is {{position}}",
+          { position }
+        )}
+      </p>
+      <ReactExampleGlobal>
+        {t("I'm a button from the application global component set")}
+      </ReactExampleGlobal>
+
+      {/* We always use aria-label for icon only buttons */}
+      <PrimaryButton icon={<PlusCircleIcon />} onClick={showModal} aria-label={t('Open Modal')} />
+
+      {/* Example of displaying data loaded with useLoadData using LoadingErrorWrapper */}
+      <div className="react-example__history-section">
+        <h3 className="react-example__history-section-title">
+          {t('Editor History (useLoadData + LoadingErrorWrapper example)')}
+        </h3>
+        <LoadingErrorWrapper loading={loadingHistory} errors={errorConfig}>
+          {historyData && (
+            <div>
+              <p>{t('Total history items: {{total}}', { total: historyData.total })}</p>
+              <Button onClick={reloadHistory}>{t('Reload History')}</Button>
+              {historyData.history?.slice(0, 3).map(item => (
+                <div key={item.id} className="react-example__history-item">
+                  <span className="react-example__history-item-status">{item.status}</span> -{' '}
+                  {item.statement.substring(0, 50)}...
+                </div>
+              ))}
+            </div>
+          )}
+        </LoadingErrorWrapper>
       </div>
-    </React.StrictMode>
+
+      {/* The title and content of the Modal is internationalized using the t-function */}
+      <Modal title={t('Modify')} open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
+        <p>{t('Settings')}</p>
+      </Modal>
+    </div>
   );
 };
 

+ 8 - 1
desktop/core/src/desktop/js/jest/jest.init.js

@@ -124,7 +124,14 @@ process.on('unhandledRejection', err => {
   fail(err);
 });
 
-jest.mock('../utils/i18nReact');
+jest.mock('../utils/i18nReact', () => ({
+  i18nReact: {
+    useTranslation: () => ({
+      t: key => key, // The mock t() function just returns the key
+      ready: true
+    })
+  }
+}));
 jest.mock('../utils/hueAnalytics');
 
 //Official workaround for TypeError: window.matchMedia is not a function

+ 19 - 1
desktop/core/src/desktop/js/reactComponents/ReactExampleGlobal/ReactExampleGlobal.scss

@@ -1,3 +1,21 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 'variables' as vars;
+
 .react-example-global {
-  background-color: lavender;
+  background-color: vars.$fluidx-gray-100;
 }

+ 86 - 8
desktop/core/src/desktop/js/reactComponents/ReactExampleGlobal/ReactExampleGlobal.test.tsx

@@ -1,18 +1,40 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 userEvent from '@testing-library/user-event';
 import '@testing-library/jest-dom';
 
 import ReactExampleGlobal from './ReactExampleGlobal';
+import hueAnalytics from '../../utils/hueAnalytics';
 
 describe('ReactExampleGlobal', () => {
-  // Make sure no unwanted console info are displayed during testing
-  const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
+  // Suppress console.info logs during all tests
+  const originalConsoleInfo = console.info;
 
-  afterEach(() => consoleSpy.mockClear());
+  beforeAll(() => {
+    console.info = jest.fn();
+  });
 
-  test('disables after click', async () => {
-    // It is recommended to call userEvent.setup per test
+  afterAll(() => {
+    console.info = originalConsoleInfo;
+  });
+
+  it('disables button after being clicked', async () => {
     const user = userEvent.setup();
     render(<ReactExampleGlobal />);
     const btn = screen.getByRole('button', { name: 'ReactExampleGlobal - Yes' });
@@ -22,7 +44,7 @@ describe('ReactExampleGlobal', () => {
     expect(btn).toBeDisabled();
   });
 
-  test('provides click callback', async () => {
+  it('calls provided onClick callback when clicked', async () => {
     const user = userEvent.setup();
     const clickCallback = jest.fn();
     render(<ReactExampleGlobal onClick={clickCallback} />);
@@ -31,11 +53,67 @@ describe('ReactExampleGlobal', () => {
     expect(clickCallback).toHaveBeenCalled();
   });
 
-  test('prints to console.info on click', async () => {
+  it('prints to console.info on click with provided props', async () => {
     const user = userEvent.setup();
+
     render(<ReactExampleGlobal version="1" myObj={{ id: 'a' }} />);
     const btn = screen.getByRole('button', { name: 'ReactExampleGlobal - Yes' });
     await user.click(btn);
-    expect(consoleSpy).toHaveBeenCalledWith('ReactExampleGlobal clicked  1 a');
+
+    expect(console.info).toHaveBeenCalledWith('ReactExampleGlobal clicked  1 a');
+  });
+
+  it('handles undefined myObj gracefully', async () => {
+    const user = userEvent.setup();
+
+    render(<ReactExampleGlobal version="1" />);
+    const btn = screen.getByRole('button', { name: 'ReactExampleGlobal - Yes' });
+    await user.click(btn);
+
+    expect(console.info).toHaveBeenCalledWith('ReactExampleGlobal clicked  1 undefined');
+  });
+
+  it('applies custom className when provided', () => {
+    render(<ReactExampleGlobal className="custom-class" />);
+    const btn = screen.getByRole('button', { name: 'ReactExampleGlobal - Yes' });
+    expect(btn).toHaveClass('custom-class');
+    expect(btn).toHaveClass('react-example-global'); // Should still have base class
+  });
+
+  it('renders custom children instead of default text', () => {
+    render(<ReactExampleGlobal>Custom Text</ReactExampleGlobal>);
+    expect(
+      screen.getByRole('button', { name: 'ReactExampleGlobal - Custom Text' })
+    ).toBeInTheDocument();
+  });
+
+  it('renders with default text when no children provided', () => {
+    render(<ReactExampleGlobal />);
+    expect(screen.getByRole('button', { name: 'ReactExampleGlobal - Yes' })).toBeInTheDocument();
+  });
+
+  it('maintains disabled state after multiple clicks', async () => {
+    const user = userEvent.setup();
+    render(<ReactExampleGlobal />);
+    const btn = screen.getByRole('button', { name: 'ReactExampleGlobal - Yes' });
+
+    await user.click(btn);
+    expect(btn).toBeDisabled();
+
+    // Try clicking again - should remain disabled
+    await user.click(btn);
+    expect(btn).toBeDisabled();
+  });
+
+  it('logs analytics event on click', async () => {
+    const analyticsSpy = jest.spyOn(hueAnalytics, 'log').mockImplementation();
+    const user = userEvent.setup();
+
+    render(<ReactExampleGlobal />);
+    const btn = screen.getByRole('button', { name: 'ReactExampleGlobal - Yes' });
+    await user.click(btn);
+
+    expect(analyticsSpy).toHaveBeenCalledWith('test-area', 'button click', true);
+    analyticsSpy.mockRestore();
   });
 });

+ 19 - 2
desktop/core/src/desktop/js/reactComponents/ReactExampleGlobal/ReactExampleGlobal.tsx

@@ -1,3 +1,19 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 strict';
 
 import React, { useState } from 'react';
@@ -19,7 +35,8 @@ const ReactExampleGlobal = ({
   onClick,
   children,
   version = '1',
-  myObj
+  myObj,
+  className
 }: ReactExampleGlobalProps): JSX.Element => {
   const [isClicked, setIsClicked] = useState(false);
 
@@ -30,7 +47,7 @@ const ReactExampleGlobal = ({
 
   return (
     <button
-      className="react-example-global"
+      className={`react-example-global ${className || ''}`}
       disabled={isClicked}
       onClick={e => {
         onClick && onClick(e);

+ 26 - 0
desktop/core/src/desktop/js/utils/hooks/useLoadData/__mocks__/useLoadData.ts

@@ -0,0 +1,26 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { jest } from '@jest/globals';
+
+const useLoadData = jest.fn(() => ({
+  data: { history: [], total: 0 },
+  loading: false,
+  error: undefined,
+  reloadData: jest.fn()
+}));
+
+export default useLoadData;