Skip to content

BED-6021 Explore Graph Data Table - Manage Columns #1602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 129 commits into from
Jul 11, 2025

Conversation

dcairnsspecterops
Copy link
Contributor

@dcairnsspecterops dcairnsspecterops commented Jun 23, 2025

Description

  • Does not add combobox to DoodleUI. Will move it if we find we need it in a second place.
  • Adds Manage Columns combobox to explore page table view.
  • Adds visible columns to redux local storage for browser-level persistence.
  • Adds simple search functionality to explore page table view (by displayname and objectid).
  • Adds 'expand' functionality to explore page table view.
  • Changes shape of useSigmaExploreGraph return value to include node_keys and nodes.

Motivation and Context

Users should be able to
Resolves: BED-6021
(Also resolves BED-6020 and BED-6018)

How Has This Been Tested?

Tests have been updates and changes have been tested manually.

Screenshots (if appropriate):

Screen.Recording.2025-07-02.at.2.52.44.PM.mov

Types of changes

  • Chore (a change that does not modify the application functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • Documentation updates are needed, and have been made accordingly.
  • I have added and/or updated tests to cover my changes.
  • All new and existing tests passed.
  • My changes include a database migration.

Summary by CodeRabbit

  • New Features

    • Added the ability to manage visible columns in the Explore Table, including selecting, pinning, and resetting columns via a new column management interface.
    • User-selected column preferences are now saved and restored automatically.
  • Enhancements

    • Explore Table columns now have improved labeling and formatting for both known and unknown properties.
    • Table filtering and search functionality have been optimized for better performance and usability.
    • Graph queries and data hooks updated to support inclusion of node properties and improved data structure handling.
    • Column management UI integrated into table controls for seamless user interaction.
  • Bug Fixes

    • Improved handling of missing or unknown node properties in graph data.
  • Documentation

    • Updated type definitions and utility exports for improved developer experience.

benwaples added 30 commits May 27, 2025 14:51
@@ -66,5 +76,6 @@ export const useExploreGraph = (includeProperties?: boolean) => {
autoHideDuration: SNACKBAR_DURATION_LONG,
});
},
enabled,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that adding this enabled field at the end of the properties will override the enabled defined within each queryConfig, which results in the query trying to execute without a queryFn yet.

Perhaps we need to move the enabled field up above the queryConfig, or pass it to getQueryConfig?

export const useExploreGraph = ({
includeProperties,
enabled,
}: UseExploreGraphParams = DEFAULT_USE_EXPLORE_GRAPH_PARAMS) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i pass includeProperties: true, will enabled then be false because the fallback object isnt triggered? If so, is that intended?

const mungedData = useMemo(
() => (data && Object.keys(data).map((id) => ({ ...data?.[id]?.data, id }))) || [],
() =>
// TODO: remove id and just use objectid for onRowClick/getRowId?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should work because the selectedItem param works with a db id or objectid!

Copy link
Contributor

@TheNando TheNando left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still digging through this, but I'll leave some notes of what I've seen so far.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/javascript/bh-shared-ui/src/components/ExploreTable/ManageColumnsComboBox/ManageColumnsComboBox.tsx (2)

94-103: Simplify the handleSelectAll logic.

The function calls handleResetDefault() in both branches, making the conditional logic redundant as mentioned in previous reviews.

Apply this diff to streamline the logic:

 const handleSelectAll = () => {
+    handleResetDefault();
     if (shouldSelectAll) {
-        handleResetDefault();
         setSelectedColumns([...allColumns]);
         onChange([...allColumns]);
-    } else {
-        handleResetDefault();
     }
-    return shouldSelectAll;
 };

148-160: Fix potential React key warnings in mapped items.

The .map() callback doesn't return anything for pinned items (line 159), which could cause React warnings about undefined array elements.

Apply this diff to fix the mapping issue:

-            ...selectedColumns.map((column, index) => {
-                if (!column?.isPinned) {
-                    return (
-                        <ManageColumnsListItem
-                            isSelected
-                            key={`${column?.id}-${index}`}
-                            item={column}
-                            onClick={removeSelectedItem}
-                            itemProps={getItemProps({ item: column, index })}
-                        />
-                    );
-                }
-            }),
+            ...selectedColumns
+                .filter(column => !column?.isPinned)
+                .map((column, index) => (
+                    <ManageColumnsListItem
+                        isSelected
+                        key={`${column?.id}-${index}`}
+                        item={column}
+                        onClick={removeSelectedItem}
+                        itemProps={getItemProps({ item: column, index })}
+                    />
+                )),
🧹 Nitpick comments (3)
packages/javascript/bh-shared-ui/src/components/ExploreTable/ManageColumnsComboBox/ManageColumnsComboBox.tsx (3)

53-62: Optimize useMultipleSelection initial state.

The initialSelectedItems on line 54 recomputes the same value as initialColumns (line 29-32), causing unnecessary work. Use the memoized value instead.

Apply this diff to use the memoized value:

     const { getDropdownProps, removeSelectedItem, addSelectedItem } = useMultipleSelection({
-        initialSelectedItems: allColumns.filter((item) => selectedColumnsProp[item.id]),
+        initialSelectedItems: initialColumns,
         selectedItems: selectedColumns,
         onStateChange({ selectedItems: newSelectedColumns, type }) {
             if (type !== useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace) {
                 setSelectedColumns(newSelectedColumns || []);
                 onChange(newSelectedColumns || []);
             }
         },
     });

72-77: Improve state consistency in ItemClick handler.

When adding a new item, the component updates selectedColumns but doesn't call onChange until the onStateChange callback, creating a potential timing issue where local state and parent state could be briefly out of sync.

Consider removing the direct state update here since onStateChange will handle it:

                 case useCombobox.stateChangeTypes.ItemClick:
                     if (newSelectedItem) {
-                        setSelectedColumns([...selectedColumns, newSelectedItem]);
+                        addSelectedItem(newSelectedItem);
                         setInputValue('');
                     }
                     break;

117-118: Consider extracting dropdown styling to constants.

The hardcoded positioning (top-3) and width (w-[400px]) values should be extracted to constants or made configurable through props for better maintainability.

Consider extracting these values:

+const DROPDOWN_POSITIONING = 'top-3';
+const DROPDOWN_WIDTH = 'w-[400px]';
+
-            <div className={`${isOpen ? '' : 'hidden'} absolute z-20 top-3`} ref={ref}>
-                <div className='w-[400px] shadow-md border-1 bg-white' {...getComboboxProps()}>
+            <div className={`${isOpen ? '' : 'hidden'} absolute z-20 ${DROPDOWN_POSITIONING}`} ref={ref}>
+                <div className={`${DROPDOWN_WIDTH} shadow-md border-1 bg-white`} {...getComboboxProps()}>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b31b77 and 9423161.

📒 Files selected for processing (2)
  • packages/javascript/bh-shared-ui/src/components/ExploreTable/ManageColumnsComboBox/ManageColumnsComboBox.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/components/ExploreTable/ManageColumnsComboBox/ManageColumnsListItem.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/javascript/bh-shared-ui/src/components/ExploreTable/ManageColumnsComboBox/ManageColumnsListItem.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/javascript/bh-shared-ui/src/components/ExploreTable/ManageColumnsComboBox/ManageColumnsComboBox.tsx (2)
packages/javascript/bh-shared-ui/src/components/ExploreTable/index.ts (1)
  • ManageColumnsComboBoxOption (18-18)
packages/javascript/bh-shared-ui/src/components/ExploreTable/explore-table-utils.ts (1)
  • makeStoreMapFromColumnOptions (3-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-analysis
  • GitHub Check: build-ui
  • GitHub Check: run-tests
🔇 Additional comments (2)
packages/javascript/bh-shared-ui/src/components/ExploreTable/ManageColumnsComboBox/ManageColumnsComboBox.tsx (2)

1-16: Well-structured imports and type definitions.

The imports are appropriate for the component functionality, and the ManageColumnsComboBoxOption type is well-defined with clear properties. The component props interface is clean and type-safe.


29-40: Good state synchronization implementation.

The useEffect on lines 33-35 properly synchronizes the local selectedColumns state with the prop changes, addressing the previous concern about desynchronization. The memoized initialColumns ensures efficient re-computation only when dependencies change.

Copy link
Contributor

@TheNando TheNando left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really appreciate all the effort you put in to this PR. This deals with some pretty complicated situations. I have a handful of suggestions that I hope can improve upon the foundation you've laid out.

Comment on lines +56 to +59
export const useExploreGraph = ({
includeProperties = DEFAULT_USE_EXPLORE_GRAPH_PARAMS.includeProperties,
enabled = DEFAULT_USE_EXPLORE_GRAPH_PARAMS.enabled,
}: UseExploreGraphParams = DEFAULT_USE_EXPLORE_GRAPH_PARAMS) => {
Copy link
Contributor

@TheNando TheNando Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constants are typically a good idea, but in this case, I'd argue for simplicity.

export const useExploreGraph = ({ includeProperties = false, enabled = true }: UseExploreGraphParams) => {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue here is that the parameter needs a default, since this existing hook is already called throughout the app with no parameters. The alternative is to find every use of this hook and pass it an empty object(?), or check for the existence of the object inside the function, which ended up feeling messier than this (since now the object needs a name and we need to pull the vars inside the call)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point. 👍

includeProperties?: boolean;
enabled?: boolean;
};

export function exploreGraphQueryFactory(paramOptions: Partial<ExploreQueryParams>): ExploreGraphQuery {
Copy link
Contributor

@TheNando TheNando Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like cypherSearchGraphQuery unfortunately does not completely align with the others queries. It has an extra param of includeProperties which means its type is not actually ExploreGraphQuery. It looks like there is no easy way around that. But we can make a small improvement to the typing to reflect what's happening.

export function exploreGraphQueryFactory(paramOptions: Partial<ExploreQueryParams>): ExploreGraphQuery | CypherExploreGraphQuery {

With that, we could make a second small improvement to not need the type assertion on line 68

    const queryConfig =
        params?.searchType === 'cypher'
            ? query.getQueryConfig(params, includeProperties)
            : query.getQueryConfig(params);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's much nicer, thanks.

Comment on lines 53 to 63
if (typeof value === 'boolean') {
return value ? (
<div className='h-full w-full flex justify-center items-center text-center'>
<FontAwesomeIcon icon={faCheck} color='green' className='scale-125' />{' '}
</div>
) : (
<div className='h-full w-full flex justify-center items-center text-center'>
<FontAwesomeIcon icon={faCancel} color='lightgray' className='scale-125' />{' '}
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use ternaries to keep this a little DRYer.

Suggested change
if (typeof value === 'boolean') {
return value ? (
<div className='h-full w-full flex justify-center items-center text-center'>
<FontAwesomeIcon icon={faCheck} color='green' className='scale-125' />{' '}
</div>
) : (
<div className='h-full w-full flex justify-center items-center text-center'>
<FontAwesomeIcon icon={faCancel} color='lightgray' className='scale-125' />{' '}
</div>
);
}
if (typeof value === 'boolean') {
return (
<div className='h-full w-full flex justify-center items-center text-center'>
<FontAwesomeIcon
icon={value ? faCheck : faCancel}
color={value ? 'green' : 'lightgray'}
className='scale-125'
/>{' '}
</div>
);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, an obviously good idea which, I feel like if I were the reviewer I would have caught, but somehow slipped my awareness in my own code. Thanks much.

Comment on lines 94 to 99
const filterKeys: (keyof GraphNode)[] = ['displayname', 'objectid'];
const filterTargets = filterKeys.map((filterKey) => {
const stringyValue = String(potentialRow?.[filterKey]);

return stringyValue?.toLowerCase();
});
Copy link
Contributor

@TheNando TheNando Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small suggestions here. The definition on line 94 can be moved outside of the component as a module constant. This way, it's not recreated for every row. If you use the ordering on line 141, you could use the const there as well, since the order would matter in that case: ...['objectid', 'displayname'].map(makeColumnDef),.

And lines 95-99 can be compressed into a 1 liner. String() will always return a string value no matter what you throw at it, so no need for the optional chaining operator (?.). The optional chains are another place where the small performance overhead adds up, so ideally, they should only be used when really needed.

const filterTargets = filterKeys.map((filterKey) => String(item[filterKey]).toLowerCase());

[nonRequiredColumnDefinitions, selectedColumns]
);

const requiredColumnDefinitions = useMemo(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all the items in this value are static, instead of the memo, you can just move this to a constant outside of the component within the module scope. Then you could remove the value from the deps below to reduce some renders.

Comment on lines 171 to 183
const tableHeaderProps: DataTableProps['TableHeaderProps'] = useMemo(
() => ({
className: 'sticky top-0 z-10',
}),
[]
);

const tableHeadProps: DataTableProps['TableHeadProps'] = useMemo(
() => ({
className: 'pr-4',
}),
[]
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming these were memoized because they are object values that were causing the data table to re-render on every ExploreTable render. If so, this is another scenario you could move the values to module-scope constants above the component. useMemo isn't free, so in cases where you aren't using component-scope members, constants are a great go-to over memos.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, yes, totally slipped my mind. useMemo is ridiculous here for simple consts, thanks for the catch

@@ -15,3 +15,5 @@
// SPDX-License-Identifier: Apache-2.0

export { default as ExploreTable } from './ExploreTable';
export type { ManageColumnsComboBoxOption } from './ManageColumnsComboBox/ManageColumnsComboBox';
Copy link
Contributor

@TheNando TheNando Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, but be aware that if the exports were to include non-types, you'd want to only preface the types themselves, otherwise you would not be able to use the non-type members for anything other than static type analysis.

// myFunc will not be emitted in js, can only be used for type info
export type { MyType, myFunc } from 'utils';

// Can use myFunc for either, as it will be emitted in js
export { type MyType, myFunc } from 'utils';

@@ -267,12 +267,18 @@ export type GraphNode = {
label: string;
kind: string;
objectId: string;
objectid?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to manually transform these keys? I feel like this is really compromising the type's integrity, specifically this key which is just a lower-case version of another key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted about this on slack; this key comes from the properties object. We now type the properties object separately, and export a SpreadNodeWithProperties interface to find this compromise.

descendent_count?: number | null;
properties?: Record<string, any>;
};
} & Record<string, any>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addendum effectively says that a GraphNode object can have any combination of key-value pairs outside the definition here, really weakening the type. Could you elaborate on the issue you encountered that lead you to this? If this is to solve a specific case, we may be better served converting this to a interface or creating an extended type as to not so drastically hinder the original type.

@@ -285,15 +291,15 @@ export type GraphEdge = {
impactPercent?: number;
exploreGraphId?: string;
data?: Record<string, any>;
};
} & Record<string, any>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here. We should avoid this, if possible. Maybe there is another solution.

@dcairnsspecterops dcairnsspecterops merged commit 5d4285e into main Jul 11, 2025
9 checks passed
@dcairnsspecterops dcairnsspecterops deleted the BED-6021-combo-box branch July 11, 2025 17:34
@github-actions github-actions bot locked and limited conversation to collaborators Jul 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants