Skip to content

Commit

Permalink
Group management node counts (#409)
Browse files Browse the repository at this point in the history
* fix build errors from bh-shared-ui

* swap memberCounts to individual

* simplify types and update tests

* white space and remove unused import

* fix name

* change endpoint

* slightly better types

* update comment

* default param instead of fallback

* dont double negative

* add missing mock

* fix import name
  • Loading branch information
benwaples authored Feb 20, 2024
1 parent 075dccd commit 70fe21b
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 152 deletions.
6 changes: 5 additions & 1 deletion cmd/ui/src/views/GroupManagement/GroupManagement.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ import { act, render, waitFor } from '../../test-utils';
import GroupManagement from './GroupManagement';
import { rest } from 'msw';
import { createMockDomain } from 'src/mocks/factories';
import { createMockAssetGroup, createMockAssetGroupMembers } from 'bh-shared-ui';
import { createMockAssetGroup, createMockAssetGroupMembers, createMockMemberCounts } from 'bh-shared-ui';
import userEvent from '@testing-library/user-event';

const domain = createMockDomain();
const assetGroup = createMockAssetGroup();
const assetGroupMembers = createMockAssetGroupMembers();
const memberCounts = createMockMemberCounts();

const server = setupServer(
rest.get('/api/v2/available-domains', (req, res, ctx) => {
Expand All @@ -43,6 +44,9 @@ const server = setupServer(
})
);
}),
rest.get('/api/v2/asset-groups/1/members/counts', (req, res, ctx) => {
return res(ctx.json(memberCounts));
}),
rest.get('*', (req, res, ctx) => res(ctx.json({ data: [] })))
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,18 @@
// SPDX-License-Identifier: Apache-2.0

import { setupServer } from 'msw/node';
import { createMockAssetGroup, createMockAssetGroupMembers, createMockSearchResults } from '../../mocks/factories';
import { createMockAssetGroup, createMockMemberCounts, createMockSearchResults } from '../../mocks/factories';
import { act, render, waitFor } from '../../test-utils';
import { AUTOCOMPLETE_PLACEHOLDER } from './AssetGroupAutocomplete';
import AssetGroupEdit from './AssetGroupEdit';
import { rest } from 'msw';
import userEvent from '@testing-library/user-event';

const assetGroup = createMockAssetGroup();
const assetGroupMembers = createMockAssetGroupMembers();
const searchResults = createMockSearchResults();
const memberCounts = createMockMemberCounts();

const server = setupServer(
rest.get('/api/v2/asset-groups/1/members', (req, res, ctx) => {
return res(
ctx.json({
count: assetGroupMembers.members.length,
limit: 100,
skip: 0,
data: assetGroupMembers,
})
);
}),
rest.get('/api/v2/search', (req, res, ctx) => {
return res(
ctx.json({
Expand All @@ -54,7 +44,7 @@ describe('AssetGroupEdit', () => {
const setup = async () => {
const user = userEvent.setup();
const screen = await act(async () => {
return render(<AssetGroupEdit assetGroup={assetGroup} filter={{}} makeNodeFilterable={() => ({})} />);
return render(<AssetGroupEdit assetGroup={assetGroup} filter={{}} memberCounts={memberCounts} />);
});
return { user, screen };
};
Expand All @@ -68,7 +58,7 @@ describe('AssetGroupEdit', () => {
it('should display a total count of asset group members', async () => {
const { screen } = await setup();
const count = screen.getByText('Total Count').nextSibling.textContent;
expect(count).toBe(assetGroupMembers.members.length.toString());
expect(count).toBe(memberCounts.total_count.toString());
});

it('should display search results when the user enters text', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,26 @@
// SPDX-License-Identifier: Apache-2.0

import { Box, Paper } from '@mui/material';
import { AssetGroup, AssetGroupMemberParams, UpdateAssetGroupSelectorRequest } from 'js-client-library';
import {
AssetGroup,
AssetGroupMemberParams,
AssetGroupMemberCounts,
UpdateAssetGroupSelectorRequest,
} from 'js-client-library';
import { FC, useEffect, useState } from 'react';
import { AssetGroupChangelog, AssetGroupChangelogEntry, ChangelogAction } from './types';
import AssetGroupAutocomplete from './AssetGroupAutocomplete';
import { SubHeader } from '../../views/Explore';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { useMutation, useQueryClient } from 'react-query';
import { apiClient } from '../../utils';
import AssetGroupChangelogTable from './AssetGroupChangelogTable';
import {
ActiveDirectoryNodeKind,
ActiveDirectoryNodeKindToDisplay,
AzureNodeKind,
AzureNodeKindToDisplay,
} from '../../graphSchema';
import { useNotifications } from '../../providers';

const AssetGroupEdit: FC<{
assetGroup: AssetGroup;
filter: AssetGroupMemberParams;
makeNodeFilterable: (node: ActiveDirectoryNodeKind | AzureNodeKind) => void;
}> = ({ assetGroup, filter, makeNodeFilterable }) => {
memberCounts: AssetGroupMemberCounts | undefined;
}> = ({ assetGroup, filter, memberCounts }) => {
const [changelog, setChangelog] = useState<AssetGroupChangelog>([]);
const addRows = changelog.filter((entry) => entry.action === ChangelogAction.ADD);
const removeRows = changelog.filter((entry) => entry.action === ChangelogAction.REMOVE);
Expand Down Expand Up @@ -93,11 +92,7 @@ const AssetGroupEdit: FC<{

return (
<Box component={Paper} elevation={0} padding={1}>
<FilteredMemberCountDisplay
assetGroupId={assetGroup.id}
label='Total Count'
filter={{ environment_id: filter.environment_id }}
/>
<SubHeader label='Total Count' count={memberCounts?.total_count} />
<AssetGroupAutocomplete
assetGroup={assetGroup}
changelog={changelog}
Expand All @@ -112,67 +107,11 @@ const AssetGroupEdit: FC<{
onSubmit={() => mutation.mutate()}
/>
)}
{Object.values(ActiveDirectoryNodeKind).map((kind) => {
const { environment_id, environment_kind } = filter;
const narrowedFilter = { primary_kind: `eq:${kind}`, environment_id, environment_kind };
const label = ActiveDirectoryNodeKindToDisplay(kind) || '';

return (
<FilteredMemberCountDisplay
key={label}
assetGroupId={assetGroup.id}
label={label}
filter={narrowedFilter}
makeNodeKindFilterable={() => makeNodeFilterable(kind)}
/>
);
})}
{Object.values(AzureNodeKind).map((kind) => {
const { environment_id, environment_kind } = filter;
const narrowedFilter = { primary_kind: `eq:${kind}`, environment_id, environment_kind };
const label = AzureNodeKindToDisplay(kind) || '';

return (
<FilteredMemberCountDisplay
key={label}
assetGroupId={assetGroup.id}
label={label}
filter={narrowedFilter}
makeNodeKindFilterable={() => makeNodeFilterable(kind)}
/>
);
{Object.entries(memberCounts?.counts ?? {}).map(([kind, count]) => {
return <SubHeader key={kind} label={kind} count={count} />;
})}
</Box>
);
};

const FilteredMemberCountDisplay: FC<{
assetGroupId: number;
label: string;
filter: AssetGroupMemberParams;
makeNodeKindFilterable?: () => void;
}> = ({ assetGroupId, label, filter, makeNodeKindFilterable }) => {
const {
data: count,
isError,
isLoading,
} = useQuery(['countAssetGroupMembers', assetGroupId, filter], ({ signal }) =>
apiClient.listAssetGroupMembers(assetGroupId.toString(), filter, { signal }).then((res) => res.data.count)
);

const hasValidCount = !isLoading && !isError && count && count > 0;

useEffect(() => {
if (hasValidCount) {
makeNodeKindFilterable?.();
}
}, [hasValidCount, makeNodeKindFilterable]);

if (hasValidCount) {
return <SubHeader label={label} count={count} />;
} else {
return null;
}
};

export default AssetGroupEdit;
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@
//
// SPDX-License-Identifier: Apache-2.0

import { createMockAssetGroupMemberParams, createMockAvailableNodeKinds } from '../../mocks/factories';
import { createMockAssetGroupMemberParams, createMockMemberCounts } from '../../mocks/factories';
import { act, render } from '../../test-utils';
import AssetGroupFilters, { FILTERABLE_PARAMS } from './AssetGroupFilters';
import userEvent from '@testing-library/user-event';
import { Screen, waitFor } from '@testing-library/react';
import { AssetGroupMemberParams } from 'js-client-library';
import { ActiveDirectoryNodeKind, AzureNodeKind } from '../..';
import { AssetGroupMemberParams, AssetGroupMemberCountsResponse } from 'js-client-library';
import { ActiveDirectoryNodeKind } from '../../graphSchema';

const filterParams = createMockAssetGroupMemberParams();
const availableNodeKinds = createMockAvailableNodeKinds();
const memberCounts = createMockMemberCounts();

describe('AssetGroupEdit', () => {
const setup = async (options?: {
filterParams?: AssetGroupMemberParams;
availableNodeKinds?: Array<ActiveDirectoryNodeKind | AzureNodeKind>;
memberCounts?: AssetGroupMemberCountsResponse['data'];
}) => {
const user = userEvent.setup();
const handleFilterChange = vi.fn();
Expand All @@ -37,15 +37,15 @@ describe('AssetGroupEdit', () => {
<AssetGroupFilters
filterParams={options?.filterParams ?? {}}
handleFilterChange={handleFilterChange}
availableNodeKinds={options?.availableNodeKinds ?? []}
memberCounts={memberCounts}
/>
);
});
return { user, screen, handleFilterChange };
};

it('renders a button that expands the filter section', async () => {
const { screen, user } = await setup({ filterParams, availableNodeKinds });
const { screen, user } = await setup({ filterParams, memberCounts });
const filtersButton = screen.getByTestId('display-filters-button');
const collapsedSection = screen.getByTestId('asset-group-filter-collapsible-section');

Expand All @@ -60,7 +60,7 @@ describe('AssetGroupEdit', () => {
});

it('indicates that filters are active', async () => {
const { screen } = await setup({ filterParams, availableNodeKinds });
const { screen } = await setup({ filterParams, memberCounts });

const filtersContainer = screen.getByTestId('asset-group-filters-container');

Expand All @@ -77,7 +77,7 @@ describe('AssetGroupEdit', () => {

describe('Node Type dropdown filter', () => {
it('displays the value from filterParams.node_type', async () => {
const { screen } = await setup({ filterParams, availableNodeKinds });
const { screen } = await setup({ filterParams, memberCounts });
const nodeTypeFilter = screen.getByTestId('asset-groups-node-type-filter');
const nodeTypeFilterValue = nodeTypeFilter.firstChild?.nextSibling;

Expand All @@ -86,42 +86,44 @@ describe('AssetGroupEdit', () => {
});

it('lists all available node kinds as options to filter by', async () => {
const { screen, user } = await setup({ availableNodeKinds });
const { screen, user } = await setup({ memberCounts });

await user.click(screen.getByTestId('display-filters-button'));
await user.click(screen.getByLabelText('Node Type'));

const nodeKindList = await screen.findAllByRole('option');

expect(nodeKindList).toHaveLength(availableNodeKinds.length + 1); // +1 for the default empty value
expect(nodeKindList).toHaveLength(memberCounts.total_count + 1); // +1 for the default empty value

for (const nodeKind of availableNodeKinds) {
for (const nodeKind in memberCounts.counts) {
expect(screen.getByText(nodeKind)).toBeInTheDocument();
}
});

it('calls handleFilterChange when a node type is selected', async () => {
const { screen, user, handleFilterChange } = await setup({ availableNodeKinds });
const { screen, user, handleFilterChange } = await setup({ memberCounts });

const expectedNodeKind = ActiveDirectoryNodeKind.Domain;

await user.click(screen.getByTestId('display-filters-button'));
await user.click(screen.getByLabelText('Node Type'));
await user.click(screen.getByText(availableNodeKinds[0]));
await user.click(screen.getByText(expectedNodeKind));

expect(handleFilterChange).toBeCalledTimes(1);
expect(handleFilterChange).toHaveBeenCalledWith('primary_kind', `eq:${availableNodeKinds[0]}`);
expect(handleFilterChange).toHaveBeenCalledWith('primary_kind', `eq:${expectedNodeKind}`);
});
});

describe('Custom Member checkbox filter', () => {
it("displays the checkbox as checked if the filter params value is 'true'", async () => {
const { screen } = await setup({ filterParams: { custom_member: 'eq:true' }, availableNodeKinds });
const { screen } = await setup({ filterParams: { custom_member: 'eq:true' }, memberCounts });
const checkbox = screen.getByTestId('asset-groups-custom-member-filter');

expect((checkbox.firstChild as HTMLInputElement)?.checked).toBe(true);
});

it('invokes handleFilterChange with eq:false when clicked and custom_member filter is on', async () => {
const { screen, user, handleFilterChange } = await setup({ filterParams, availableNodeKinds });
const { screen, user, handleFilterChange } = await setup({ filterParams, memberCounts });
const checkbox = screen.getByTestId('asset-groups-custom-member-filter');

await user.click(checkbox);
Expand All @@ -143,14 +145,14 @@ describe('AssetGroupEdit', () => {

describe('Clear Filters button', () => {
it('has a button with text Clear Filters', async () => {
const { screen } = await setup({ filterParams, availableNodeKinds });
const { screen } = await setup({ filterParams, memberCounts });
const clearFilersButton = screen.getByText('Clear Filters');

expect(clearFilersButton).toBeInTheDocument();
});

it('calls handleFilterChange with all filter types and empty strings when clicked while filters are active', async () => {
const { screen, user, handleFilterChange } = await setup({ filterParams, availableNodeKinds });
const { screen, user, handleFilterChange } = await setup({ filterParams, memberCounts });
const clearFilersButton = screen.getByText('Clear Filters');

await user.click(clearFilersButton);
Expand Down
Loading

0 comments on commit 70fe21b

Please sign in to comment.