8000 Bed 5985 by elikmiller · Pull Request #1664 · SpecterOps/BloodHound · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Bed 5985 #1664

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft

Bed 5985 #1664

wants to merge 9 commits into from

Conversation

elikmiller
Copy link
Contributor
@elikmiller elikmiller commented Jul 9, 2025

Description

Describe your changes in detail

Motivation and Context

Resolves <TICKET_OR_ISSUE_NUMBER>

Why is this change required? What problem does it solve?

How Has This Been Tested?

Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc.

Screenshots (optional):

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)
  • Database Migrations

Checklist:

Summary by CodeRabbit

  • New Features

    • Introduced an infinitely scrollable, virtualized list component with dynamic sizing and seamless data loading.
    • Added a reusable hook for measuring container dimensions.
    • Implemented utility functions for paginated API data fetching.
    • Added a new selectors list component for improved zone management navigation.
  • Improvements

    • Refactored members and details list components for better performance, modularity, and clarity.
    • Enhanced data fetching with paginated and infinite query hooks for tags, selectors, and members.
    • Updated response types to support pagination.
  • Bug Fixes

    • Improved error and loading state handling in list components.
  • Chores

    • Consolidated and cleaned up React Query hooks, removing unused or redundant files and updating import paths.

Copy link
Contributor
coderabbitai bot commented Jul 9, 2025

Walkthrough

This update introduces a new virtualized infinite list component and supporting hook/utilities, refactors zone management details and member list components to use infinite queries, and consolidates all zone management-related React Query hooks into a single module. It removes now-redundant hooks files, updates API response types for pagination, and adapts tests and UI logic accordingly.

Changes

File(s) Change Summary
.../InfiniteQueryFixedList/InfiniteQueryFixedList.tsx, index.ts Added new infinite virtualized list component and index export.
.../hooks/useMeasure.tsx Added a hook to measure DOM element dimensions using ResizeObserver.
.../utils/paginatedFetcher.ts Added a generic utility for paginated API fetching.
.../Details/Details.tsx Refactored to use new hooks for data fetching, added sorting state, updated UI logic for lists and edit button.
.../Details/DetailsList.tsx, DetailsList.test.tsx Removed "Selectors" support from details list component and related tests; now only supports "Tiers" and "Labels".
.../Details/MembersList.tsx, MembersList.test.tsx Refactored members list to use infinite query and new list component; disabled related test.
.../Details/SelectorsList.tsx Added new SelectorsList component using infinite query and virtualized list.
.../Save/SelectorForm/SeedSelection.tsx, SelectorForm.tsx, types.ts Updated import paths for hooks/types.
.../Save/SelectorForm/hooks.ts, .../Save/TagForm/hooks.ts Deleted old hooks files for selectors and tags.
.../Save/TagForm/TagForm.tsx Updated imports to use new hooks.
.../hooks.ts Major expansion: consolidated and added all hooks for tags, selectors, members, CRUD, and infinite queries.
.../js-client-library/src/responses.ts Changed selectors response type to paginated.

Sequence Diagram(s)

sequenceDiagram
  participant UI as ZoneManagement UI
  participant List as InfiniteQueryFixedList
  participant Query as React Query Infinite Query
  participant API as Backend API

  UI->>Query: Initiate infinite query for items (selectors/members)
  Query->>API: Fetch first page (skip, limit)
  API-->>Query: Return items, count, nextPageParam
  Query-->>UI: Provide items and loading state
  UI->>List: Render InfiniteQueryFixedList with query result
  List->>UI: Request renderRow for visible items
  List->>Query: When near end, trigger fetchNextPage
  Query->>API: Fetch next page
  API-->>Query: Return more items
  Query-->>List: Update items, trigger re-render
Loading

Suggested labels

enhancement, user interface, api

Suggested reviewers

  • mvlipka

Poem

In the warren of code, a new list takes flight,
With infinite scroll and data so light.
Hooks now united, no more the sprawl—
Paginated fetchers answer the call.
Selectors and members, all neatly in view,
A rabbit’s delight: fresh features, brand new!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@elikmiller
Copy link
Contributor Author

@coderabbitai review

Copy link
Contributor
coderabbitai bot commented Jul 10, 2025
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 7

🧹 Nitpick comments (5)
packages/javascript/bh-shared-ui/src/hooks/useMeasure.tsx (1)

19-41: Consider explicitly typing the return value as a tuple.

The hook returns [width, height] but the return type isn't explicitly defined as a tuple. This could lead to type inference issues where TypeScript might infer the return type as number[] instead of [number, number].

-export function useMeasure(ref: RefObject<HTMLElement>) {
+export function useMeasure(ref: RefObject<HTMLElement>): [number, number] {
     const [width, setWidth] = useState(0);
     const [height, setHeight] = useState(0);
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/SelectorsList.tsx (1)

54-61: Consider optimizing window height calculation.

The getListHeight(window.innerHeight) is called multiple times on every render. Consider memoizing this value or using a resize observer to avoid unnecessary calculations.

+import { useMemo } from 'react';

 const SelectorsListWrapper = ({ children }: { children: React.ReactNode }) => {
+    const listHeight = useMemo(() => getListHeight(window.innerHeight), []);
+
     return (
         <div data-testid={`zone-management_details_selectors-list`}>
             <div
                 data-testid={`zone-management_details_selectors-list_static-order`}
                 className='p-0 relative w-full border-b-2 border-neutral-light-5 dark:border-neutral-dark-5'>
                 <div className='inline-flex items-center justify-center h-10 transition-colors text-neutral-dark-5 dark:text-neutral-light-5 pl-6 font-bold text-xl'>
                     Selectors
                 </div>
             </div>

             <div
                 className={cn(`overflow-y-auto border-x-2 border-neutral-light-5 dark:border-neutral-dark-5`, {
-                    'h-[762px]': getListHeight(window.innerHeight) === 762,
-                    'h-[642px]': getListHeight(window.innerHeight) === 642,
-                    'h-[438px]': getListHeight(window.innerHeight) === 438,
+                    'h-[762px]': listHeight === 762,
+                    'h-[642px]': listHeight === 642,
+                    'h-[438px]': listHeight === 438,
                 })}>
                 {children}
             </div>
         </div>
     );
 };
packages/javascript/bh-shared-ui/src/components/InfiniteQueryFixedList/InfiniteQueryFixedList.tsx (1)

23-30: Consider making the loading text customizable.

The loading placeholder is hardcoded as "Loading..." which might not be appropriate for all use cases. Consider adding it as an optional prop.

 export type InfiniteQueryFixedListProps<T> = {
     itemSize: number;
     queryResult: UseInfiniteQueryResult<PaginatedResult<T>>;
     renderRow: (item: T, index: number, style: React.CSSProperties, isScrolling?: boolean) => React.ReactNode;
     overscanCount?: number;
     thresholdCount?: number;
     listRef?: ForwardedRef<FixedSizeList<T[]>>;
+    loadingText?: string;
 };

And update the component:

 export const InfiniteQueryFixedList = <T,>({
     itemSize,
     queryResult,
     renderRow,
     overscanCount = 5,
     thresholdCount = 5,
     listRef,
+    loadingText = 'Loading...',
 }: InfiniteQueryFixedListProps<T>) => {
         if (!isItemLoaded(index)) {
-            return <div style={style}>Loading...</div>;
+            return <div style={style}>{loadingText}</div>;
         }

Also applies to: 56-58

packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts (2)

39-39: Consider making PAGE_SIZE configurable

The hardcoded PAGE_SIZE of 25 might not be optimal for all use cases. Different views might benefit from different page sizes.

Consider accepting page size as a parameter in the hooks or making it configurable through a context/environment variable:

-const PAGE_SIZE = 25;
+const DEFAULT_PAGE_SIZE = 25;

// Then in hooks:
-export const useSelectorsInfiniteQuery = (tagId: string | number) =>
+export const useSelectorsInfiniteQuery = (tagId: string | number, pageSize = DEFAULT_PAGE_SIZE) =>
     useInfiniteQuery<{
         items: AssetGroupTagSelector[];
         nextPageParam?: PageParam;
     }>({
         queryKey: zoneManagementKeys.selectorsByTag(tagId),
-        queryFn: ({ pageParam = { skip: 0, limit: PAGE_SIZE } }) =>
+        queryFn: ({ pageParam = { skip: 0, limit: pageSize } }) =>
             getAssetGroupTagSelectors(tagId, pageParam.skip, pageParam.limit),
         getNextPageParam: (lastPage) => lastPage.nextPageParam,
     });

99-110: Document sorting behavior for undefined sortOrder

The function maps undefined sortOrder to descending order ('-name'). This implicit behavior should be documented.

Add a comment to clarify the default behavior:

 export const getAssetGroupTagMembers = (
     tagId: number | string,
     skip = 0,
     limit = PAGE_SIZE,
     sortOrder: SortOrder = 'asc'
 ) =>
     createPaginatedFetcher<AssetGroupTagMemberListItem>(
+        // Note: undefined or any non-'asc' value defaults to descending order
         () => apiClient.getAssetGroupTagMembers(tagId, skip, limit, sortOrder === 'asc' ? 'name' : '-name'),
         'members',
         skip,
         limit
     );
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 204b34c and 7e5192d.

📒 Files selected for processing (18)
  • packages/javascript/bh-shared-ui/src/components/InfiniteQueryFixedList/InfiniteQueryFixedList.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/components/InfiniteQueryFixedList/index.ts (1 hunks)
  • packages/javascript/bh-shared-ui/src/hooks/useMeasure.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/utils/paginatedFetcher.ts (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/Details.tsx (4 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/DetailsList.test.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/DetailsList.tsx (2 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/MembersList.test.tsx (2 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/MembersList.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/SelectorsList.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/SeedSelection.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/SelectorForm.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/hooks.ts (0 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/types.ts (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/TagForm/TagForm.tsx (2 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/TagForm/hooks.ts (0 hunks)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts (1 hunks)
  • packages/javascript/js-client-library/src/responses.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/TagForm/hooks.ts
  • packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/hooks.ts
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: zinic
PR: SpecterOps/BloodHound#1573
File: packages/go/dawgs/drivers/pg/query/sql/schema_up.sql:514-581
Timestamp: 2025-06-12T15:42:11.258Z
Learning: Contributor "zinic" prefers not to receive automated refactor suggestions/comments on their pull requests.
packages/javascript/js-client-library/src/responses.ts (1)
Learnt from: mistahj67
PR: SpecterOps/BloodHound#1648
File: cmd/api/src/api/v2/assetgrouptags.go:763-766
Timestamp: 2025-07-09T00:36:54.112Z
Learning: In cmd/api/src/api/v2/assetgrouptags.go, the SearchAssetGroupTags method intentionally fetches all asset group tags and selectors without database-level filtering because it needs to build a complete `kinds` array from all relevant tags for the graph query filter. This allows members to be searched across all tags of the requested type while still filtering the returned tags/selectors by name match.
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/Details.tsx (1)
Learnt from: mistahj67
PR: SpecterOps/BloodHound#1648
File: cmd/api/src/api/v2/assetgrouptags.go:763-766
Timestamp: 2025-07-09T00:36:54.112Z
Learning: In cmd/api/src/api/v2/assetgrouptags.go, the SearchAssetGroupTags method intentionally fetches all asset group tags and selectors without database-level filtering because it needs to build a complete `kinds` array from all relevant tags for the graph query filter. This allows members to be searched across all tags of the requested type while still filtering the returned tags/selectors by name match.
packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts (1)
Learnt from: mistahj67
PR: SpecterOps/BloodHound#1648
File: cmd/api/src/api/v2/assetgrouptags.go:763-766
Timestamp: 2025-07-09T00:36:54.112Z
Learning: In cmd/api/src/api/v2/assetgrouptags.go, the SearchAssetGroupTags method intentionally fetches all asset group tags and selectors without database-level filtering because it needs to build a complete `kinds` array from all relevant tags for the graph query filter. This allows members to be searched across all tags of the requested type while still filtering the returned tags/selectors by name match.
🧬 Code Graph Analysis (6)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/TagForm/TagForm.tsx (1)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts (1)
  • useTagsQuery (66-71)
packages/javascript/js-client-library/src/responses.ts (1)
packages/javascript/js-client-library/src/types.ts (1)
  • AssetGroupTagSelector (100-110)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/MembersList.tsx (6)
packages/javascript/js-client-library/src/responses.ts (1)
  • AssetGroupTagMemberListItem (177-179)
packages/javascript/bh-shared-ui/src/types.ts (1)
  • SortOrder (27-27)
packages/javascript/bh-shared-ui/src/components/InfiniteQueryFixedList/InfiniteQueryFixedList.tsx (2)
  • InfiniteQueryFixedListProps (23-30)
  • InfiniteQueryFixedList (32-85)
packages/javascript/bh-shared-ui/src/utils/theme.ts (1)
  • cn (41-43)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/utils.tsx (2)
  • SelectedHighlight (25-35)
  • getListHeight (51-56)
packages/javascript/bh-shared-ui/src/components/ColumnHeaders/ColumnHeaders.tsx (1)
  • SortableHeader (60-100)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/Details.tsx (9)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Summary/Summary.tsx (1)
  • getEditButtonState (29-35)
packages/javascript/bh-shared-ui/src/utils/searchParams/useAppNavigate.ts (1)
  • useAppNavigate (25-40)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/utils.tsx (2)
  • TIER_ZERO_ID (20-20)
  • getTagUrlValue (23-25)
packages/javascript/bh-shared-ui/src/types.ts (1)
  • SortOrder (27-27)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts (4)
  • useTagsQuery (66-71)
  • useSelectorsInfiniteQuery (88-97)
  • useSelectorMembersInfiniteQuery (144-158)
  • useTagMembersInfiniteQuery (112-121)
packages/javascript/js-client-library/src/types.ts (3)
  • AssetGroupTagTypeTier (56-56)
  • AssetGroupTagTypeLabel (57-57)
  • AssetGroupTagTypeOwned (58-58)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/DetailsList.tsx (1)
  • DetailsList (57-167)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/SelectorsList.tsx (1)
  • SelectorsList (74-139)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/MembersList.tsx (1)
  • MembersList (44-101)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/DetailsList.tsx (2)
packages/javascript/js-client-library/src/types.ts (1)
  • AssetGroupTag (76-85)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/utils.tsx (1)
  • isTag (37-39)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts (6)
packages/javascript/js-client-library/src/requests.ts (3)
  • CreateAssetGroupTagRequest (35-41)
  • UpdateAssetGroupTagRequest (43-43)
  • RequestOptions (26-26)
packages/javascript/js-client-library/src/types.ts (2)
  • AssetGroupTag (76-85)
  • AssetGroupTagSelector (100-110)
packages/javascript/bh-shared-ui/src/utils/paginatedFetcher.ts (2)
  • createPagi 8000 natedFetcher (37-50)
  • PageParam (17-20)
packages/javascript/bh-shared-ui/src/types.ts (1)
  • SortOrder (27-27)
packages/javascript/js-client-library/src/responses.ts (1)
  • AssetGroupTagMemberListItem (177-179)
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/types.ts (3)
  • CreateSelectorParams (26-29)
  • PatchSelectorParams (35-37)
  • DeleteSelectorParams (30-33)
🔇 Additional comments (16)
packages/javascript/bh-shared-ui/src/utils/paginatedFetcher.ts (1)

17-50: LGTM! Well-structured pagination utility.

The implementation is clean and follows React Query conventions:

  • Clear type definitions for pagination parameters and responses
  • Correct pagination logic for determining if more pages exist
  • Proper Promise-based API that integrates well with React Query infinite queries
  • Generic typing allows for reusable implementation across different data types
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/SeedSelection.tsx (1)

29-29: LGTM! Import path updated to use centralized hooks.

The import path change correctly reflects the consolidation of hooks from the local ./hooks file to the centralized ../../hooks module, improving code organization and maintainability.

packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/types.ts (1)

17-17: LGTM! Cleaner import path.

The import path change from 'js-client-library/dist/requests' to 'js-client-library' is an improvement that uses a more direct and maintainable import path from the package root.

packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/SelectorForm/SelectorForm.tsx (1)

28-28: LGTM! Import path updated to use centralized hooks.

The import path change correctly reflects the consolidation of hooks from the local ./hooks file to the centralized ../../hooks module, maintaining functionality while improving code organization.

packages/javascript/bh-shared-ui/src/components/InfiniteQueryFixedList/index.ts (1)

17-17: LGTM! Clean component entry point.

The re-export pattern follows standard practices for component organization, allowing clean imports from the directory level.

packages/javascript/bh-shared-ui/src/views/ZoneManagement/Save/TagForm/TagForm.tsx (2)

44-50: Good refactoring to improve modularity.

The replacement of the single useAssetGroupTags hook with multiple specific hooks (useAssetGroupTagInfo, useCreateAssetGroupTag, useDeleteAssetGroupTag, usePatchAssetGroupTag, useTagsQuery) improves separation of concerns and makes the API more granular and reusable.


112-112: LGTM! Consistent with the new hook architecture.

The change from useAssetGroupTags() to useTagsQuery() maintains the same functionality while aligning with the refactored hook structure.

packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/DetailsList.test.tsx (1)

73-73: Test updates correctly reflect component scope changes.

The change from 'Selectors' to 'Labels' in the test props aligns with the removal of selector support from the DetailsList component, ensuring tests remain valid for the updated component interface.

Also applies to: 81-81

packages/javascript/js-client-library/src/responses.ts (1)

189-189: Appropriate type change to support pagination.

Changing from BasicResponse to PaginatedResponse correctly provides the necessary pagination metadata (count, limit, skip) required by the infinite query hooks and pagination components.

packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/SelectorsList.tsx (1)

74-139: Well-structured component implementation.

The component properly handles loading, error, and success states, integrates well with the InfiniteQueryFixedList, and provides appropriate visual feedback for disabled selectors.

packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/DetailsList.tsx (2)

42-47: Clean refactoring to narrow component scope.

The type restrictions properly limit this component to only handle 'Tiers' and 'Labels', making the component's purpose clearer and the API more type-safe.


148-156: Consistent refactoring removing selector-specific logic.

The removal of disabled state styling and tooltip handling is appropriate since this component no longer handles selectors (which had the disabled_at field).

packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/Details.tsx (1)

79-90: Excellent use of custom React Query hooks.

The implementation properly uses the new infinite query hooks with appropriate filtering for tags and sort order support for members. This provides a clean separation of data fetching logic from the UI component.

packages/javascript/bh-shared-ui/src/components/InfiniteQueryFixedList/InfiniteQueryFixedList.tsx (1)

32-85: Well-implemented generic infinite scrolling component.

The component demonstrates excellent use of React patterns:

  • Proper memoization of callbacks and computed values
  • Clean integration with react-window
  • Good handling of container measurements
  • Appropriate use of TypeScript generics
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/MembersList.tsx (1)

26-35: Excellent refactoring to use declarative data fetching.

The refactoring successfully:

  • Removes internal state management complexity
  • Delegates infinite loading to the reusable component
  • Improves separation of concerns by accepting query results as props
  • Maintains all the original functionality in a cleaner implementation

Also applies to: 44-50

packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts (1)

262-270: Fix enabled condition to handle numeric tagId correctly

The enabled condition only checks for empty string, but tagId can be a number. A valid tagId of 0 would pass this check incorrectly.

Update the condition to handle both types:

 export const useAssetGroupTagInfo = (tagId: string) =>
     useQuery({
         queryKey: zoneManagementKeys.tagDetail(tagId),
         queryFn: async ({ signal }) => {
             const response = await apiClient.getAssetGroupTag(tagId, { signal });
             return response.data.data.tag;
         },
-        enabled: tagId !== '',
+        enabled: tagId !== '' && tagId !== undefined,
     });
⛔ Skipped due to learnings
Learnt from: mistahj67
PR: SpecterOps/BloodHound#1648
File: cmd/api/src/api/v2/assetgrouptags.go:763-766
Timestamp: 2025-07-09T00:36:54.112Z
Learning: In cmd/api/src/api/v2/assetgrouptags.go, the SearchAssetGroupTags method intentionally fetches all asset group tags and selectors without database-level filtering because it needs to build a complete `kinds` array from all relevant tags for the graph query filter. This allows members to be searched across all tags of the requested type while still filtering the returned tags/selectors by name match.

import { render, screen, waitFor } from '../../../test-utils';
import { apiClient } from '../../../utils';
import { MembersList } from './MembersList';
// import { apiClient } from '../../../utils';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update tests to match the refactored component implementation.

The tests have been commented out following the MembersList component refactor, leaving the component without test coverage. This creates a risk of undetected regressions.

The tests should be updated to work with the new implementation that uses UseInfiniteQueryResult and the InfiniteQueryFixedList component. Would you like me to help generate updated test cases that match the new architecture?

Also applies to: 39-39, 42-61

🤖 Prompt for AI Agents
In
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/MembersList.test.tsx
around lines 20, 39, and 42 to 61, the existing tests are commented out and
outdated due to the MembersList component refactor using UseInfiniteQueryResult
and InfiniteQueryFixedList. Update the test cases to mock and assert behavior
based on the new infinite query data fetching pattern and the
InfiniteQueryFixedList rendering, ensuring coverage of loading states, data
fetching, and list rendering according to the new component structure.

Comment on lines +67 to +73
* @description This component is meant to display the lists for either Tiers, Labels, or Selectors but not the Members list since that is a paginated list that loads more data as a user scrolls.
* @param {object} props
* @param {UseQueryResult} props.listQuery The endpoint call result wrapper from react query that allows us to hook into different states that the fetched data could be in
* @param {selected} props.selected The id of the particular entity that is selected for the list. It is used for selected item rendering
* @param {(id:number) => void} props.onSelect The click handler that should be called when an item from this list is selected. This is primarily being used to set the selected id state in the parent Details component
* @returns The component that displays a list of entities for the zone management page
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect JSDoc comment.

The JSDoc comment states this component displays "Tiers, Labels, or Selectors" but this component is specifically designed only for Selectors. This appears to be copy-pasted from the DetailsList component.

 /**
- * @description This component is meant to display the lists for either Tiers, Labels, or Selectors but not the Members list since that is a paginated list that loads more data as a user scrolls.
+ * @description This component displays a list of Selectors with infinite scrolling support.
  * @param {object} props
  * @param {UseQueryResult} props.listQuery The endpoint call result wrapper from react query that allows us to hook into different states that the fetched data could be in
  * @param {selected} props.selected The id of the particular entity that is selected for the list. It is used for selected item rendering
  * @param {(id:number) => void} props.onSelect The click handler that should be called when an item from this list is selected. This is primarily being used to set the selected id state in the parent Details component
- * @returns The component that displays a list of entities for the zone management page
+ * @returns The component that displays a list of selectors for the zone management page
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @description This component is meant to display the lists for either Tiers, Labels, or Selectors but not the Members list since that is a paginated list that loads more data as a user scrolls.
* @param {object} props
* @param {UseQueryResult} props.listQuery The endpoint call result wrapper from react query that allows us to hook into different states that the fetched data could be in
* @param {selected} props.selected The id of the particular entity that is selected for the list. It is used for selected item rendering
* @param {(id:number) => void} props.onSelect The click handler that should be called when an item from this list is selected. This is primarily being used to set the selected id state in the parent Details component
* @returns The component that displays a list of entities for the zone management page
*/
/**
* @description This component displays a list of Selectors with infinite scrolling support.
* @param {object} props
* @param {UseQueryResult} props.listQuery The endpoint call result wrapper from react query that allows us to hook into different states that the fetched data could be in
* @param {selected} props.selected The id of the particular entity that is selected for the list. It is used for selected item rendering
* @param {(id:number) => void} props.onSelect The click handler that should be called when an item from this list is selected. This is primarily being used to set the selected id state in the parent Details component
* @returns The component that displays a list of selectors for the zone management page
*/
🤖 Prompt for AI Agents
In
packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/SelectorsList.tsx
around lines 67 to 73, the JSDoc comment incorrectly states that the component
displays "Tiers, Labels, or Selectors," which is inaccurate as this component is
specifically for Selectors only. Update the JSDoc comment to reflect that this
component displays only the Selectors list, removing references to Tiers and
Labels to accurately describe its purpose.

Comment on lines +53 to 64
export const getEditButtonState = (
memberId?: string,
selectorsQuery?: UseQueryResult,
tiersQuery?: UseQueryResult,
labelsQuery?: UseQueryResult
) => {
if (selectorId !== undefined) {
const selectedSelector = selectorsQuery.data?.find((selector) => {
return selectorId === selector.id.toString();
});
return selectedSelector?.counts?.members || 0;
} else if (tagId !== undefined) {
const selectedTag = tagsQuery.data?.find((tag) => {
return tagId === tag.id.toString();
});
return selectedTag?.counts?.members || 0;
} else {
return 0;
}
};

export const getEditButtonState = (memberId?: string, selectorsQuery?: UseQueryResult, tagsQuery?: UseQueryResult) => {
return (
!!memberId ||
(selectorsQuery?.isLoading && tagsQuery?.isLoading) ||
(selectorsQuery?.isError && tagsQuery?.isError)
(selectorsQuery?.isLoading && tiersQuery?.isLoading && labelsQuery?.isLoading) ||
(selectorsQuery?.isError && tiersQuery?.isError && labelsQuery?.isError)
);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Review the edit button state logic.

The current implementation only disables the edit button when ALL queries are loading or ALL queries have errors. This might not be the intended behavior - typically you'd want to disable the button if ANY query is loading or has an error.

 export const getEditButtonState = (
     memberId?: string,
     selectorsQuery?: UseQueryResult,
     tiersQuery?: UseQueryResult,
     labelsQuery?: UseQueryResult
 ) => {
     return (
         !!memberId ||
-        (selectorsQuery?.isLoading && tiersQuery?.isLoading && labelsQuery?.isLoading) ||
-        (selectorsQuery?.isError && tiersQuery?.isError && labelsQuery?.isError)
+        (selectorsQuery?.isLoading || tiersQuery?.isLoading || labelsQuery?.isLoading) ||
+        (selectorsQuery?.isError || tiersQuery?.isError || labelsQuery?.isError)
     );
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getEditButtonState = (
memberId?: string,
selectorsQuery?: UseQueryResult,
tiersQuery?: UseQueryResult,
labelsQuery?: UseQueryResult
) => {
if (selectorId !== undefined) {
const selectedSelector = selectorsQuery.data?.find((selector) => {
return selectorId === selector.id.toString();
});
return selectedSelector?.counts?.members || 0;
} else if (tagId !== undefined) {
const selectedTag = tagsQuery.data?.find((tag) => {
return tagId === tag.id.toString();
});
return selectedTag?.counts?.members || 0;
} else {
return 0;
}
};
export const getEditButtonState = (memberId?: string, selectorsQuery?: UseQueryResult, tagsQuery?: UseQueryResult) => {
return (
!!memberId ||
(selectorsQuery?.isLoading && tagsQuery?.isLoading) ||
(selectorsQuery?.isError && tagsQuery?.isError)
(selectorsQuery?.isLoading && tiersQuery?.isLoading && labelsQuery?.isLoading) ||
(selectorsQuery?.isError && tiersQuery?.isError && labelsQuery?.isError)
);
};
export const getEditButtonState = (
memberId?: string,
selectorsQuery?: UseQueryResult,
tiersQuery?: UseQueryResult,
labelsQuery?: UseQueryResult
) => {
return (
!!memberId ||
(selectorsQuery?.isLoading || tiersQuery?.isLoading || labelsQuery?.isLoading) ||
(selectorsQuery?.isError || tiersQuery?.isError || labelsQuery?.isError)
);
};
🤖 Prompt for AI Agents
In packages/javascript/bh-shared-ui/src/views/ZoneManagement/Details/Details.tsx
around lines 53 to 64, the getEditButtonState function disables the edit button
only when all queries are loading or all have errors, but it should disable the
button if any query is loading or has an error. Update the logic to use OR
conditions for loading and error checks across selectorsQuery, tiersQuery, and
labelsQuery, so the button is disabled if any one of these queries is loading or
has an error.

Comment on lines +185 to +192
export const usePatchSelector = (tagId: string | number | undefined) => {
const queryClient = useQueryClient();
return useMutation(patchSelector, {
onSettled: () => {
queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId!));
},
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation for undefined tagId in patch mutation hook

Same issue as useCreateSelector - the hook accepts optional tagId but uses non-null assertion.

Apply the same fix:

 export const usePatchSelector = (tagId: string | number | undefined) => {
     const queryClient = useQueryClient();
     return useMutation(patchSelector, {
         onSettled: () => {
+            if (tagId === undefined) return;
-            queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId!));
+            queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId));
         },
     });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const usePatchSelector = (tagId: string | number | undefined) => {
const queryClient = useQueryClient();
return useMutation(patchSelector, {
onSettled: () => {
queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId!));
},
});
};
export const usePatchSelector = (tagId: string | number | undefined) => {
const queryClient = useQueryClient();
return useMutation(patchSelector, {
onSettled: () => {
if (tagId === undefined) return;
queryClient.invalidateQueries(
zoneManagementKeys.selectorsByTag(tagId)
);
},
});
};
🤖 Prompt for AI Agents
In packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts around
lines 185 to 192, the usePatchSelector hook uses a non-null assertion on tagId
without validating if tagId is undefined, which can cause runtime errors. Fix
this by adding a validation check for tagId before calling
queryClient.invalidateQueries, ensuring the function only runs if tagId is
defined, similar to the fix applied in useCreateSelector.

Comment on lines +123 to +142
export const getAssetGroupSelectorMembers = (
tagId: number | string,
selectorId: number | string | undefined = undefined,
skip: number = 0,
limit: number = PAGE_SIZE,
sortOrder: SortOrder = 'asc'
) =>
createPaginatedFetcher(
() =>
apiClient.getAssetGroupTagSelectorMembers(
tagId,
selectorId!,
skip,
limit,
sortOrder === 'asc' ? 'name' : '-name'
),
'members',
skip,
limit
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Non-null assertion on potentially undefined selectorId

The function accepts selectorId as an optional parameter that defaults to undefined, but uses a non-null assertion operator (!) on line 134. This will cause a runtime error when selectorId is undefined.

Either make selectorId required or add proper handling:

 export const getAssetGroupSelectorMembers = (
     tagId: number | string,
-    selectorId: number | string | undefined = undefined,
+    selectorId: number | string,
     skip: number = 0,
     limit: number = PAGE_SIZE,
     sortOrder: SortOrder = 'asc'
 ) =>
     createPaginatedFetcher(
         () =>
             apiClient.getAssetGroupTagSelectorMembers(
                 tagId,
-                selectorId!,
+                selectorId,
                 skip,
                 limit,
                 sortOrder === 'asc' ? 'name' : '-name'
             ),
         'members',
         skip,
         limit
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getAssetGroupSelectorMembers = (
tagId: number | string,
selectorId: number | string | undefined = undefined,
skip: number = 0,
limit: number = PAGE_SIZE,
sortOrder: SortOrder = 'asc'
) =>
createPaginatedFetcher(
() =>
apiClient.getAssetGroupTagSelectorMembers(
tagId,
selectorId!,
skip,
limit,
sortOrder === 'asc' ? 'name' : '-name'
),
'members',
skip,
limit
);
export const getAssetGroupSelectorMembers = (
tagId: number | string,
selectorId: number | string,
skip: number = 0,
limit: number = PAGE_SIZE,
sortOrder: SortOrder = 'asc'
) =>
createPaginatedFetcher(
() =>
apiClient.getAssetGroupTagSelectorMembers(
tagId,
selectorId,
skip,
limit,
sortOrder === 'asc' ? 'name' : '-name'
),
'members',
skip,
limit
);
🤖 Prompt for AI Agents
In packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts around
lines 123 to 142, the function getAssetGroupSelectorMembers uses a non-null
assertion on the optional parameter selectorId, which can be undefined and cause
runtime errors. To fix this, either make selectorId a required parameter by
removing the default undefined and non-null assertion, or add conditional logic
to handle the case when selectorId is undefined before passing it to
apiClient.getAssetGroupTagSelectorMembers.

Comment on lines +168 to +175
export const useCreateSelector = (tagId: string | number | undefined) => {
const queryClient = useQueryClient();
return useMutation(createSelector, {
onSettled: () => {
queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId!));
},
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation for undefined tagId in mutation hook

The hook accepts optional tagId but uses non-null assertion when invalidating queries. This could cause runtime errors.

Add validation:

 export const useCreateSelector = (tagId: string | number | undefined) => {
     const queryClient = useQueryClient();
     return useMutation(createSelector, {
         onSettled: () => {
+            if (tagId === undefined) return;
-            queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId!));
+            queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId));
         },
     });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useCreateSelector = (tagId: string | number | undefined) => {
const queryClient = useQueryClient();
return useMutation(createSelector, {
onSettled: () => {
queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId!));
},
});
};
export const useCreateSelector = (tagId: string | number | undefined) => {
const queryClient = useQueryClient();
return useMutation(createSelector, {
onSettled: () => {
if (tagId === undefined) return;
queryClient.invalidateQueries(zoneManagementKeys.selectorsByTag(tagId));
},
});
};
🤖 Prompt for AI Agents
In packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts around
lines 168 to 175, the useCreateSelector hook uses a non-null assertion on tagId
when invalidating queries, which can cause runtime errors if tagId is undefined.
Add a validation check to ensure tagId is defined before calling
queryClient.invalidateQueries, and handle the case when tagId is undefined
appropriately to avoid runtime exceptions.

Comment on lines +144 to +158
export const useSelectorMembersInfiniteQuery = (
tagId: number | string,
selectorId: number | string | undefined,
sortOrder: SortOrder
) =>
useInfiniteQuery<{
items: AssetGroupTagMemberListItem[];
nextPageParam?: PageParam;
}>({
queryKey: zoneManagementKeys.membersByTagAndSelector(tagId, selectorId),
queryFn: ({ pageParam = { skip: 0, limit: PAGE_SIZE } }) =>
getAssetGroupSelectorMembers(tagId, selectorId, pageParam.skip, pageParam.limit, sortOrder),
getNextPageParam: (lastPage) => lastPage.nextPageParam,
enabled: selectorId !== undefined,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve type safety for selector members query

The hook correctly uses enabled: selectorId !== undefined to prevent execution, but still passes potentially undefined selectorId to getAssetGroupSelectorMembers. This creates a fragile dependency on the execution order.

Consider this safer approach:

 export const useSelectorMembersInfiniteQuery = (
     tagId: number | string
10000
,
     selectorId: number | string | undefined,
     sortOrder: SortOrder
 ) =>
     useInfiniteQuery<{
         items: AssetGroupTagMemberListItem[];
         nextPageParam?: PageParam;
     }>({
         queryKey: zoneManagementKeys.membersByTagAndSelector(tagId, selectorId),
-        queryFn: ({ pageParam = { skip: 0, limit: PAGE_SIZE } }) =>
-            getAssetGroupSelectorMembers(tagId, selectorId, pageParam.skip, pageParam.limit, sortOrder),
+        queryFn: ({ pageParam = { skip: 0, limit: PAGE_SIZE } }) => {
+            if (selectorId === undefined) {
+                throw new Error('selectorId is required');
+            }
+            return getAssetGroupSelectorMembers(tagId, selectorId, pageParam.skip, pageParam.limit, sortOrder);
+        },
         getNextPageParam: (lastPage) => lastPage.nextPageParam,
         enabled: selectorId !== undefined,
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const useSelectorMembersInfiniteQuery = (
tagId: number | string,
selectorId: number | string | undefined,
sortOrder: SortOrder
) =>
useInfiniteQuery<{
items: AssetGroupTagMemberListItem[];
nextPageParam?: PageParam;
}>({
queryKey: zoneManagementKeys.membersByTagAndSelector(tagId, selectorId),
queryFn: ({ pageParam = { skip: 0, limit: PAGE_SIZE } }) =>
getAssetGroupSelectorMembers(tagId, selectorId, pageParam.skip, pageParam.limit, sortOrder),
getNextPageParam: (lastPage) => lastPage.nextPageParam,
enabled: selectorId !== undefined,
});
export const useSelectorMembersInfiniteQuery = (
tagId: number | string,
selectorId: number | string | undefined,
sortOrder: SortOrder
) =>
useInfiniteQuery<{
items: AssetGroupTagMemberListItem[];
nextPageParam?: PageParam;
}>({
queryKey: zoneManagementKeys.membersByTagAndSelector(tagId, selectorId),
queryFn: ({ pageParam = { skip: 0, limit: PAGE_SIZE } }) => {
if (selectorId === undefined) {
throw new Error('selectorId is required');
}
return getAssetGroupSelectorMembers(tagId, selectorId, pageParam.skip, pageParam.limit, sortOrder);
},
getNextPageParam: (lastPage) => lastPage.nextPageParam,
enabled: selectorId !== undefined,
});
🤖 Prompt for AI Agents
In packages/javascript/bh-shared-ui/src/views/ZoneManagement/hooks.ts around
lines 144 to 158, the hook passes selectorId directly to
getAssetGroupSelectorMembers even when it might be undefined, risking runtime
errors. To fix this, add a runtime check to ensure selectorId is defined before
calling getAssetGroupSelectorMembers, or adjust the query function to only
execute when selectorId is non-undefined, preventing unsafe calls and improving
type safety.

@elikmiller
Copy link
Contributor Author

This is about as far as I am gonna get on this pagination story before heading out on PTO for a few days

  • I haven't added new or fixed existing tests except to unblock the build
  • There are lots of places where I need to update the JSdocs
  • There are merge conflicts
  • I don't know if the API changes to add pagination support for the selectors endpoint are in yet but the UI should work either way

I'd be happy to pick this back up when I return or let someone else chew on it if it needs to move forward - I will defer to @mistahj67 or @urangel to decide

@elikmiller
Copy link
Contributor Author

One small thing I wanted to add back was using the DoodleUI Skeleton component in the new InfiniteQueryFixedList instead of the "Loading..." string

@elikmiller elikmiller self-assigned this Jul 10, 2025
@elikmiller elikmiller added user interface A pull request containing changes affecting the UI code. enhancement New feature or request labels Jul 10, 2025
Comment on lines +41 to +53
export const zoneManagementKeys = {
all: ['zone-management'] as const,
tags: () => [...zoneManagementKeys.all, 'tags'] as const,
tagDetail: (tagId: string | number) => [...zoneManagementKeys.tags(), 'tagId', tagId] as const,
selectors: () => [...zoneManagementKeys.all, 'selectors'] as const,
selectorsByTag: (tagId: string | number) => [...zoneManagementKeys.selectors(), 'tag', tagId] as const,
selectorDetail: (tagId: string | number, selectorId: string | number) =>
[...zoneManagementKeys.selectorsByTag(tagId), 'selectorId', selectorId] as const,
members: () => [...zoneManagementKeys.all, 'members'] as const,
membersByTag: (tagId: string | number) => [...zoneManagementKeys.members(), 'tag', tagId] as const,
membersByTagAndSelector: (tagId: string | number, selectorId: string | number | undefined) =>
[...zoneManagementKeys.membersByTag(tagId), 'selector', selectorId] as const,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

awyiss

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request user interface A pull request containing changes affecting the UI code.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0