Skip to content

Ae glossary domain color picker#17838

Open
annadoesdesign wants to merge 13 commits into
masterfrom
ae--glossary-domain-color-picker
Open

Ae glossary domain color picker#17838
annadoesdesign wants to merge 13 commits into
masterfrom
ae--glossary-domain-color-picker

Conversation

@annadoesdesign

@annadoesdesign annadoesdesign commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a modern color picker to domains and glossary entities (terms + nodes), modernizes the create/edit modals onto Alchemy, makes the glossary sidebar collapsible, and tightens the color-inheritance behavior so the icon you see on a term matches the icon on its profile header everywhere it appears.

What's changing

User-facing

  • Color picker on create + edit. Domains, glossary nodes, and glossary terms can now have a color set when they're created and changed later from the entity header (replaces the legacy antd picker with the Alchemy ColorPicker). The default picker color is violet500 (#533FD1).
  • Inheritance with override. A term's own color wins; otherwise the term inherits its root parent's color; otherwise a deterministic palette color seeded from the parent URN; otherwise from the term URN. This is now applied consistently in the sidebar, entity header, search results, browse cards, and GlossaryColoredIcon consumers.
  • Color-picker default = parent's color. When creating a child under a group, the picker pre-fills with the parent's color so children inherit the group's identity by default. Once the user explicitly picks a color, that choice is locked (colorWasPicked flag) and won't shift if they change the parent.
  • Inline documentation in create modal. Replaces the previous modal-in-modal pattern — the rich-text editor is now embedded directly in the Create Glossary modal.
  • Optional parent on create. You can create a new glossary node/term without selecting a parent.
  • Modern parent selector. NodeParentSelect is rewritten on top of Alchemy SimpleSelect with tree-style browse + search (powered by useGlossaryTreeEntities); V1 re-exports V2 so there's one source of truth.
  • Collapsible glossary sidebar. Mirrors the domains sidebar — animated width, grouped header actions, colored entity icons visible in the collapsed state.
  • Sidebar horizontal scrollbar fixed. Pre-existing layout bug where StyledDivider's width: calc(100% + 26px + depth*18px) paired with margin-left: calc(-13px - depth*18px) overshot the sidebar by 13px on the right.

Frontend (technical)

  • Migrate touched files from antd to Alchemy (toast, Button, Input, Text, Modal, ColorPicker, SimpleSelect, Editor, Loader) and Phosphor icons (PencilSimple, BookmarkSimple, BookmarksSimple, CaretDown/Right, ArrowLineLeft/Right, etc.).
  • DefaultEntityHeader renders DomainColoredIcon / GlossaryColoredIcon, prioritizes displayProperties.colorHex with a palette fallback, and removes the legacy yellow ribbon.
  • Propagate term displayProperties through GlossaryEntitiesList / GlossaryEntityItem / GlossaryListCard, and prioritize own colorHex over inherited iconColor in NodeItem / TermItem.
  • Always render glossary nodes with BookmarksSimple (nested nodes were falling back to the term icon).
  • colorWasPicked flag in create modals avoids persisting the gray placeholder default and overriding the deterministic palette color.
  • Pure helpers (getGlossaryTermColor, buildTermTreeOptions, filterResultsForMove) extracted into .ts files for direct unit testing.
  • Fix React hooks-order violation in EntityPage by hoisting renderProfile outside the conditional branch.

GraphQL

  • Add displayProperties to GlossaryTerm in entity.graphql.
  • Add displayProperties to childGlossaryTerm fragment, nested layers in rootGlossaryNodeWithFourLayers, and GlossaryTerm / GlossaryNode in searchResultsWithoutSchemaField so sidebar / list / autocomplete views fetch color data.
  • Query displayProperties in getGlossaryTerm.

Backend

  • entity-registry.yml: add displayProperties as a valid aspect on glossaryTerm.
  • GlossaryNodeType / GlossaryTermType: include DISPLAY_PROPERTIES_ASPECT_NAME in ASPECTS_TO_RESOLVE so GMS hydrates/persists displayProperties.
  • GlossaryTermMapper: map displayProperties onto GlossaryTerm.
  • UpdateDisplayPropertiesResolver: entity-aware authorization (canManageGlossaries for glossary terms/nodes, canManageDomains otherwise); fix SLF4J placeholder bug in error messages.
  • GmsGraphQLEngine: pass EntityClient to UpdateDisplayPropertiesResolver.

Tests

  • Added unit tests for getGlossaryTermColor precedence rules (colorUtils.test.ts).
  • Added unit tests for useTermTreeOptions color inheritance (useTermTreeOptions.test.tsx).
  • Added unit tests for filterResultsForMove move-mode filtering (NodeParentSelect.test.tsx).
  • yarn type-check, yarn eslint --quiet, and the affected vitest suites (43/43) all pass.

Checklist

  • PR conforms to the Contributing Guideline (incl. PR Title Format)
  • Tests added/updated
  • Docs added/updated (no user-docs changes needed for this UI work)
  • Breaking changes documented in docs/how/updating-datahub.md (not breaking — new aspect field is additive)

annadoesdesign and others added 3 commits June 9, 2026 08:48
Introduce a modern color picker (Alchemy ColorPicker) for domains, glossary nodes,
and glossary terms. Users can now choose a color when creating an entity and edit
it later from the entity header. The picker shows the entity's saved color (or
the deterministic palette color) so editing reflects current state.

Frontend
- Replace legacy color picker with Alchemy ColorPicker in IconColorPicker,
  CreateDomainModal, and CreateGlossaryEntityModal (V1 + V2).
- Migrate easy antd usages to Alchemy (toast, Button, Input, Text, Modal) and
  replace antd icons with Phosphor (PencilSimple, SquaresFour, FileText,
  ListBullets, Columns, ArrowLineLeft/Right, CaretDown/Right, BookmarkSimple,
  BookmarksSimple).
- DefaultEntityHeader: render DomainColoredIcon/GlossaryColoredIcon, use
  BookmarkSimple for terms and BookmarksSimple for nodes, prioritize
  displayProperties.colorHex with palette fallback, remove yellow ribbon.
- Guard against persisting the default color when the user did not explicitly
  pick one (colorWasPicked flag in create modals).
- Propagate term displayProperties through GlossaryEntitiesList /
  GlossaryEntityItem / GlossaryListCard, and prioritize own colorHex over
  inherited iconColor in NodeItem / TermItem.
- Make GlossarySidebar collapsible (mirrors domain sidebar): grouped header
  actions, animated width, colored entity icons rendered when collapsed.
- Set theme colorPickerDefault to violet500 (#533FD1).
- Fix React hooks order violation in EntityPage by hoisting renderProfile
  outside the conditional branch.

GraphQL
- Add displayProperties to GlossaryTerm in entity.graphql.
- Add displayProperties to childGlossaryTerm fragment, nested GlossaryNode
  layers in rootGlossaryNodeWithFourLayers, and GlossaryTerm in
  searchResultsWithoutSchemaField so sidebar/list views fetch color data.
- Query displayProperties in getGlossaryTerm.

Backend
- entity-registry.yml: add displayProperties as a valid aspect on glossaryTerm.
- GlossaryNodeType / GlossaryTermType: include DISPLAY_PROPERTIES_ASPECT_NAME
  in ASPECTS_TO_RESOLVE so GMS hydrates/persists displayProperties.
- GlossaryTermMapper: map displayProperties onto GlossaryTerm.
- UpdateDisplayPropertiesResolver: entity-aware authorization
  (canManageGlossaries for glossary terms/nodes, canManageDomains otherwise);
  fix SLF4J placeholder bug in error messages.
- GmsGraphQLEngine: pass EntityClient to UpdateDisplayPropertiesResolver.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Rewrite NodeParentSelect on top of alchemy SimpleSelect with browse + search
  through useGlossaryTreeEntities; V1 re-exports V2 for a single source of truth
- Default the color picker in the create modal to the selected parent's color,
  but lock to the user's choice once they pick one
- Inline the documentation rich-text editor in the create modal instead of the
  modal-in-modal pattern; make parent selection optional
- Route glossary term colors through getGlossaryTermColor everywhere
  (own colorHex -> root parent's -> palette of parent URN -> palette of term URN),
  fixing the sidebar/header color mismatch on term profile pages
- Always render glossary nodes with BookmarksSimple (nested nodes were falling
  back to the term icon)
- Query displayProperties on terms in autoComplete/facet/sidebar fragments so
  the inheritance chain has data to walk
- Add i18n keys for color/icon/optional, the parent search placeholder, and
  the glossary sidebar expand/collapse tooltips

Co-authored-by: Cursor <cursoragent@cursor.com>
…roll

Aligns recent glossary/color-picker work to Chris Collins' conventions
(toast + i18n error handling, no React.FC, testable helpers in sibling
.ts files) and fixes a pre-existing sidebar layout bug.

- IconColorPicker: drop React.FC for a function declaration, i18n the
  modal title and all toast strings, type catch as (e: unknown), and
  remove console.error noise (match EntityName.tsx).
- CreateGlossaryEntityModal (V1 + V2): i18n the name-length validation
  string under the matching createModal/createGlossary namespaces.
- NodeParentSelect: switch to styled-components/macro and extract
  filterResultsForMove into a sibling utils file so the .tsx exports
  only its component (fixes a react-refresh warning, follows Chris's
  pattern for test-exposed pure helpers).
- GlossarySidebar: document why collapsed root-level items skip the
  full getGlossaryTermColor inheritance chain.
- GlossaryBrowser: fix horizontal scrollbar caused by StyledDivider's
  width: calc(100% + 26px + depth*18px) / margin-left: calc(-13px -
  depth*18px) formula overshooting its container by 13px on the right.
  Replace with width: 100% and clamp BrowserWrapper to overflow-y only.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added the product PR or Issue related to the DataHub UI/UX label Jun 9, 2026
Resolved conflicts from #17782 (i18n V1 entity dropdown keys deduplication):

- entityV1.shared.entityDropdown.json: accept deletion (master removed
  the V1 namespace; our `createModal.nameMaxLengthError` migrated to
  `createGlossary.nameMaxLengthError` which the auto-merged V2 file
  already preserves).
- entity/shared/EntityDropdown/CreateGlossaryEntityModal.tsx: keep our
  Alchemy rewrite (toast, ColorPicker, inline Editor, alchemy Modal/
  Input/Text) and rename remaining `createModal.*` keys to the
  deduped `createGlossary.*` equivalents (including param rename
  `message` -> `errorMessage` on createGlossary.error).

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

PR Title Check Failed

Your PR title must follow the format: <type>[optional scope]: <description>

Examples:

  • feat(ingestion): add Snowflake v2 source
  • fix: resolve crash on empty dashboard

See the Contributing Guide for allowed types and format details.

@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
23326 4 23322 191
View the top 3 failed test(s) by shortest run time
tests.unit.sdk.test_rest_emitter.TestDataHubRestEmitter::test_openapi_emitter_emit_mcps_max_items
Stack Traces | 0.102s run time
self = <test_rest_emitter.TestDataHubRestEmitter object at 0x7f6035fbb1d0>
openapi_emitter = DataHubRestEmitter: configured to talk to http://fakegmshost:8080

    def test_openapi_emitter_emit_mcps_max_items(self, openapi_emitter):
        def mock_emit_response(*args, **kwargs):
            resp = Mock(spec=Response)
            resp.status_code = 200
            resp.headers = {}
            resp.json.return_value = []
            return resp
    
        with (
            patch(
                "datahub.emitter.rest_emitter.DataHubRestEmitter._emit_generic",
                side_effect=mock_emit_response,
            ) as mock_emit,
            warnings.catch_warnings(),
        ):
            warnings.simplefilter("ignore", APITracingWarning)
            # Create more items than BATCH_INGEST_MAX_PAYLOAD_LENGTH
            items = [
                MetadataChangeProposalWrapper(
                    entityUrn=f"urn:li:dataset:(urn:li:dataPlatform:mysql,Item{i},PROD)",
                    aspect=DatasetProfile(
                        rowCount=i,
                        columnCount=15,
                        timestampMillis=1626995099686,
                    ),
                )
                for i in range(
                    BATCH_INGEST_MAX_PAYLOAD_LENGTH + 2
                )  # Create 2 more than max
            ]
    
            openapi_emitter.emit_mcps(items)
    
            # Verify multiple chunks were created
>           assert mock_emit.call_count == 2
E           AssertionError: assert 3 == 2
E            +  where 3 = <MagicMock name='_emit_generic' id='140050709358864'>.call_count

.../unit/sdk/test_rest_emitter.py:353: AssertionError
glossary sidebar navigation test cypress/e2e/glossaryV2/v2_glossary_navigation.js::cypress/e2e/glossaryV2/v2_glossary_navigation.js
Stack Traces | 28.9s run time
2026-06-11T17:37:58.039Z
Timed out retrying after 10000ms: Expected to find element: `:visible`, but never found it. Queried from:

              > cy.contains(CypressGlosssaryNavigationTerm)
tests.cypress.integration_test::test_run_cypress
Stack Traces | 285s run time
auth_session = <tests.utils.TestSessionWrapper object at 0x7fa09bc747d0>

    def test_run_cypress(auth_session):
        # Run with --record option only if CYPRESS_RECORD_KEY is non-empty
        record_key = env_vars.get_cypress_record_key()
        tag_arg = ""
        test_strategy = env_vars.get_test_strategy()
        if record_key:
            record_arg = " --record "
            batch_number = env_vars.get_batch_number()
            batch_count = env_vars.get_batch_count()
            if batch_count > 1:
                batch_suffix = f"-{batch_number}{batch_count}"
            else:
                batch_suffix = ""
            tag_arg = f" --tag {test_strategy}{batch_suffix}"
        else:
            record_arg = " "
    
        logger.info(f"test strategy is {test_strategy}")
        test_spec_arg = ""
        specs_str = ",".join([f"**/{f}" for f in _get_filtered_or_batched_tests()])
        test_spec_arg = f" --spec '{specs_str}' "
    
        logger.info("Running Cypress tests with command")
        node_options = "--max-old-space-size=500"
        electron_args = 'ELECTRON_EXTRA_LAUNCH_ARGS="--js-flags=\'--max-old-space-size=4096 --disable-dev-shm-usage --disable-gpu --no-sandbox"'
        command = f'{electron_args} NO_COLOR=1 NODE_OPTIONS="{node_options}" npx cypress run {record_arg} {test_spec_arg} {tag_arg}'
        logger.info(command)
        # Add --headed --spec '**/mutations/mutations.js' (change spec name)
        # in case you want to see the browser for debugging
        print_now()
        proc = subprocess.Popen(
            command,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            cwd=f"{CYPRESS_TEST_DATA_DIR}",
            text=True,  # Use text mode for string output
            bufsize=1,  # Line buffered
        )
        assert proc.stdout is not None
        assert proc.stderr is not None
    
        # Function to read and print output from a pipe
        def read_and_print(pipe, prefix=""):
            for line in pipe:
                logger.info(f"{prefix}{line.rstrip()}")
    
        # Read and print output in real-time
    
        stdout_thread = threading.Thread(target=read_and_print, args=(proc.stdout,))
        stderr_thread = threading.Thread(
            target=read_and_print, args=(proc.stderr, "stderr: ")
        )
    
        # Set threads as daemon so they exit when the main thread exits
        stdout_thread.daemon = True
        stderr_thread.daemon = True
    
        # Start the threads
        stdout_thread.start()
        stderr_thread.start()
    
        # Wait for the process to complete
        return_code = proc.wait()
    
        # Wait for the threads to finish
        stdout_thread.join()
        stderr_thread.join()
    
        logger.info(f"return code: {return_code}")
        print_now()
>       assert return_code == 0
E       assert 1 == 0

tests/cypress/integration_test.py:363: AssertionError
glossary/v2-glossary-navigation.spec.ts::glossary sidebar navigation › can move a term group under a parent node
Stack Traces | 361s run time
Test timeout of 180000ms exceeded.
glossary/v2-glossary-navigation.spec.ts::glossary sidebar navigation › can move a term into its parent term group
Stack Traces | 361s run time
Test timeout of 180000ms exceeded.

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@alwaysmeticulous

alwaysmeticulous Bot commented Jun 9, 2026

Copy link
Copy Markdown

🔴 Meticulous spotted visual differences in 306 of 1304 screens tested: view and approve differences detected.

Meticulous evaluated ~10 hours of user flows against your PR.

Last updated for commit b622d3b Merge branch 'master' into ae--glossary-domain-color-picker. This comment will update as new commits are pushed.

@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.77kB (0.05%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
datahub-react-web-esm 23.9MB 12.77kB (0.05%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: datahub-react-web-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/index-*.js 12.26kB 8.93MB 0.14%
assets/en-*.js 510 bytes 273.48kB 0.19%

Files in assets/index-*.js:

  • ./src/app/entity/shared/EntityDropdown/MoveGlossaryEntityModal.tsx → Total Size: 3.76kB

  • ./src/app/glossaryV2/GlossaryEntitiesList.tsx → Total Size: 2.37kB

  • ./src/app/entityV2/EntityPage.tsx → Total Size: 2.98kB

  • ./src/app/entityV2/shared/containers/profile/header/EntityName.tsx → Total Size: 5.11kB

  • ./src/app/domainV2/CreateDomainModal.tsx → Total Size: 7.47kB

  • ./src/app/entityV2/shared/EntityDropdown/nodeParentSelectUtils.ts → Total Size: 199 bytes

  • ./src/alchemy-components/components/Toast/Toast.tsx → Total Size: 4.89kB

  • ./src/app/glossaryV2/BusinessGlossaryPage.tsx → Total Size: 3.49kB

  • ./src/app/entityV2/glossaryNode/GlossaryNodeEntity.tsx → Total Size: 3.95kB

  • ./src/app/entityV2/glossaryNode/preview/Preview.tsx → Total Size: 852 bytes

  • ./src/app/entity/shared/EntityDropdown/CreateGlossaryEntityModal.tsx → Total Size: 11.55kB

  • ./src/app/entityV2/shared/EntityDropdown/CreateGlossaryEntityModal.tsx → Total Size: 11.15kB

  • ./src/app/entityV2/shared/EntityDropdown/NodeParentSelect.tsx → Total Size: 7.85kB

  • ./src/app/entityV2/glossaryTerm/preview/Preview.tsx → Total Size: 1.27kB

  • ./src/app/entity/EntityPage.tsx → Total Size: 2.37kB

  • ./src/app/glossaryV2/GlossaryBrowser/GlossaryBrowser.tsx → Total Size: 2.66kB

  • ./src/app/glossaryV2/GlossaryBrowser/NodeItem.tsx → Total Size: 7.07kB

  • ./src/app/entityV2/shared/containers/profile/header/IconPicker/IconColorPicker.tsx → Total Size: 3.69kB

  • ./src/app/glossaryV2/GlossaryBrowser/TermItem.tsx → Total Size: 3.58kB

  • ./src/app/entityV2/shared/containers/profile/header/DefaultEntityHeader.tsx → Total Size: 6.99kB

  • ./src/app/entityV2/glossaryTerm/GlossaryTermEntity.tsx → Total Size: 5.19kB

…r defaults

- Add GlossaryEntityIcon wrapper that resolves color via the same chain the
  sidebar uses (own colorHex -> root parent -> deterministic palette) and
  pairs with the correct Phosphor icon by entity type.
- Wire it through DefaultEntityIcon and SingleEntityIcon so autocomplete,
  home V3 modules, hierarchy tree, related terms, and entity filters render
  the same colored bookmark as the glossary sidebar.
- Replace the diagonal ribbon decoration on glossary preview cards and home
  V2 entity links with the colored bookmark; delete the now-unused
  GlossaryPreviewCardDecoration component.
- Default the create-modal color picker to the first palette swatch
  (violet600 / #533FD1) instead of the unrelated violet500 (#705EE4).
- Fall through to the deterministic palette color when the selected parent
  has no explicit displayProperties.colorHex, so the picker pre-fills with
  the color the user actually sees on the parent.
- Carry the picked color into the optimistic sidebar cache entry so newly
  created glossary nodes render with the right color from the first paint
  instead of flickering through the inherited parent color until the search
  refetch catches up.

Co-authored-by: Cursor <cursoragent@cursor.com>
The sidebar create button only opened the term-group modal, which hid the
ability to create root-level terms even though DataHub supports them. Wrap
it in an Alchemy `Menu` that offers both options, using the same Phosphor
icons the sidebar already uses to render each entity type (BookmarksSimple
for groups, BookmarkSimple for terms). Reuses the existing `empty.addTerm`
and `empty.addTermGroup` i18n keys.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
GlossaryEntityItem was passing the useEntityData() wrapper (urn,
entityType, entityData, loading) into GlossaryListCard's entityData
prop, which expects the inner GenericEntityProperties. Color resolution
therefore silently missed the parent's colorHex and fell through to a
palette color derived from the child's URN, so children on the Contents
tab disagreed with the sidebar and header.

Destructure the inner entity at the caller and mirror the sidebar's
cascade in GlossaryListCard: child colorHex > parent colorHex >
palette(parentUrn || childUrn). Applies to both child terms and child
nodes.

Co-authored-by: Cursor <cursoragent@cursor.com>
annadoesdesign and others added 3 commits June 10, 2026 13:39
After this PR moved the glossary modal toasts to alchemy `toast.*` and the
edit icon to phosphor `<PencilSimple>`, several Cypress and Playwright
helpers were still keyed off antd-era selectors.

- Playwright `ToastComponent`: match both `.ant-message` (antd `message.*`)
  and the new alchemy toast container so glossary toast assertions pass
  while the rest of the codebase finishes migrating away from antd.
- Alchemy `ToastContainer`: add `data-testid="toast-notification-container"`
  as the stable hook the playwright helper looks for.
- Playwright domain `editDomainName`: assert on `aria-label` instead of
  visible text — header chrome added by the color icon now causes the
  name to ellipsize.
- Cypress `v2_glossary.js`: replace the `.ant-input-affix-wrapper` modal
  driver (no longer present after the alchemy `<Input>` migration) with
  the same `data-testid` pattern used by the other glossary specs.
- Cypress `v2_glossary{,Term,_navigation}.js`: swap `.anticon-edit` for
  `.ant-typography-edit` — the inline-edit pencil icon was replaced but
  the antd Typography editable wrapper class is stable.
- Cypress `clickOptionWithText`: filter to `:visible` matches first so a
  link inside a hidden tab pane doesn't win the lookup and fail visibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review Label for PRs that need review from a maintainer. product PR or Issue related to the DataHub UI/UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants