Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
16 changes: 16 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2230,6 +2230,22 @@ class ApiMethods {
};
};

/**
* @param organization Can be the organization's ID or name
* @param options Pagination and search options
*/
getOrganizationPaginatedGroups = async (
organization: string,
options?: TypesGen.PaginatedGroupsRequest,
): Promise<TypesGen.PaginatedGroupsResponse> => {
const url = getURLWithSearchParams(
`/api/v2/organizations/${organization}/paginated-groups`,
options,
);
const response = await this.axios.get(url);
return response.data;
};

/**
* @param organization Can be the organization's ID or name
*/
Expand Down
60 changes: 60 additions & 0 deletions site/src/api/queries/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {
GroupMembersResponse,
GroupRequest,
OrganizationGroupsAISpend,
PaginatedGroupsRequest,
PaginatedGroupsResponse,
PatchGroupRequest,
UsersRequest,
} from "#/api/typesGenerated";
Expand Down Expand Up @@ -93,6 +95,37 @@ export const groupMembersAISpend = (
} satisfies UseQueryOptions<GroupMembersAISpend>;
};

const getPaginatedGroupsByOrganizationQueryKey = (
organization: string,
req?: PaginatedGroupsRequest,
) => {
// Nested under the org groups key so create/patch/delete invalidations,
// which target ["organization", org, "groups"], also cover this list.
const base = [...getGroupsByOrganizationQueryKey(organization), "paginated"];
return req ? [...base, req] : base;
};

export function paginatedGroupsByOrganization(
organization: string,
searchParams: URLSearchParams,
): UsePaginatedQueryOptions<PaginatedGroupsResponse, PaginatedGroupsRequest> {
return {
searchParams,
queryPayload: ({ limit, offset }) => {
return {
limit,
offset,
q: prepareQuery(searchParams.get("filter") ?? ""),
};
},

queryKey: ({ payload }) =>
getPaginatedGroupsByOrganizationQueryKey(organization, payload),
queryFn: ({ payload }) =>
API.getOrganizationPaginatedGroups(organization, payload),
};
}

const getRootGroupQueryKey = (organization: string, groupName: string) => [
"organization",
organization,
Expand Down Expand Up @@ -167,6 +200,33 @@ export function groupMembers(
};
}

export const getGroupMemberAvatarsQueryKey = (
organization: string,
groupName: string,
limit: number,
) => [...getGroupMembersQueryKey(organization, groupName), "avatars", limit];

/** Number of member avatars previewed per group row in list views. */
export const GROUP_MEMBER_AVATAR_LIMIT = 5;

/**
* A capped page of a group's members for avatar previews in list views. The
* paginated groups endpoint no longer returns rosters, so rows fetch a small
* preview lazily. Nests under the group members key so membership mutations
* invalidate it.
*/
export const groupMemberAvatars = (
organization: string,
groupName: string,
limit: number,
): UseQueryOptions<GroupMembersResponse> => {
return {
queryKey: getGroupMemberAvatarsQueryKey(organization, groupName, limit),
queryFn: ({ signal }) =>
API.getGroupMembers(organization, groupName, { limit }, signal),
};
};

export type GroupsByUserId = Readonly<Map<string, readonly Group[]>>;

export function groupsByUserId() {
Expand Down
18 changes: 10 additions & 8 deletions site/src/components/Filter/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,16 @@ export const Filter: FC<FilterProps> = ({
</>
) : (
<>
<PresetMenu
value={filter.query}
onSelect={(query) => filter.update(query)}
presets={presets}
learnMoreLink={learnMoreLink}
learnMoreLabel2={learnMoreLabel2}
learnMoreLink2={learnMoreLink2}
/>
{presets.length > 0 && (
<PresetMenu
value={filter.query}
onSelect={(query) => filter.update(query)}
presets={presets}
learnMoreLink={learnMoreLink}
learnMoreLabel2={learnMoreLabel2}
learnMoreLink2={learnMoreLink2}
/>
)}
<div className="flex flex-col gap-2 w-full">
<SearchField
ref={textboxInputRef}
Expand Down
19 changes: 19 additions & 0 deletions site/src/components/Filter/GroupsFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { FC } from "react";
import { Filter, type useFilter } from "#/components/Filter/Filter";

interface GroupsFilterProps {
filter: ReturnType<typeof useFilter>;
}

// GroupsFilter renders a search-only filter. Groups support free-text search
// against name and display name, so there are no presets or option menus.
export const GroupsFilter: FC<GroupsFilterProps> = ({ filter }) => {
return (
<Filter
presets={[]}
isLoading={false}
filter={filter}
optionsSkeleton={null}
/>
);
};
34 changes: 16 additions & 18 deletions site/src/pages/GroupsPage/GroupsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { PlusIcon } from "lucide-react";
import { type FC, useEffect } from "react";
import { useQuery } from "react-query";
import { Link as RouterLink } from "react-router";
import { useSearchParams } from "react-router";
import { toast } from "sonner";
import { getErrorDetail, getErrorMessage } from "#/api/errors";
import {
groupsByOrganization,
organizationGroupsAISpend,
paginatedGroupsByOrganization,
} from "#/api/queries/groups";
import { organizationsPermissions } from "#/api/queries/organizations";
import { Button } from "#/components/Button/Button";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { useFilter } from "#/components/Filter/Filter";
import { Loader } from "#/components/Loader/Loader";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "#/components/SettingsHeader/SettingsHeader";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { usePaginatedQuery } from "#/hooks/usePaginatedQuery";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
import { pageTitle } from "#/utils/page";
Expand All @@ -29,17 +29,22 @@ const GroupsPage: FC = () => {
const { template_rbac: groupsEnabled, aibridge } = useFeatureVisibility();
const { organization, showOrganizations } = useGroupsSettings();
const aibridgeVisible = Boolean(aibridge);
const groupsQuery = useQuery({
...groupsByOrganization(organization?.name ?? ""),
enabled: Boolean(organization),
const [searchParams, setSearchParams] = useSearchParams();
const groupsQuery = usePaginatedQuery(
paginatedGroupsByOrganization(organization?.name ?? "", searchParams),
);
const filter = useFilter({
searchParams,
onSearchParamsChange: setSearchParams,
onUpdate: groupsQuery.goToFirstPage,
});
const groupIds = groupsQuery.data?.map((group) => group.id) ?? [];
const groupIds = groupsQuery.data?.groups.map((group) => group.id) ?? [];
const groupsSpendQuery = useQuery({
...organizationGroupsAISpend(organization?.name ?? "", groupIds),
enabled: aibridgeVisible && Boolean(organization) && groupIds.length > 0,
});
const groupsWithSpend = joinGroupsSpend(
groupsQuery.data,
groupsQuery.data?.groups,
groupsSpendQuery.data,
);
const permissionsQuery = useQuery({
Expand Down Expand Up @@ -113,15 +118,6 @@ const GroupsPage: FC = () => {
{showOrganizations ? "organization" : "deployment"}.
</SettingsHeaderDescription>
</SettingsHeader>

{groupsEnabled && permissions.createGroup && (
<Button asChild>
<RouterLink to="create">
<PlusIcon className="size-icon-sm" />
Create group
</RouterLink>
</Button>
)}
</div>

<GroupsPageView
Expand All @@ -130,6 +126,8 @@ const GroupsPage: FC = () => {
canCreateGroup={permissions.createGroup}
groupsEnabled={groupsEnabled}
showAIBudget={aibridgeVisible}
filterProps={{ filter }}
groupsQuery={groupsQuery}
permissions={authPermissions}
/>
</div>
Expand Down
Loading
Loading