Skip to content
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

Group management node counts #409

Merged
merged 19 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }) => {
definitelynotagoblin marked this conversation as resolved.
Show resolved Hide resolved
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
Loading