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
6 changes: 6 additions & 0 deletions site/src/pages/GroupsPage/GroupMembersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
TableHeader,
TableRow,
} from "#/components/Table/Table";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { isEveryoneGroup } from "#/modules/groups";
import { cn } from "#/utils/cn";
Expand Down Expand Up @@ -77,7 +78,11 @@ const GroupMembersPage: FC = () => {
const removeMemberMutation = useMutation(
removeMember(queryClient, organization),
);
const { permissions: sitePermissions } = useAuthenticated();
const canUpdateGroup = permissions ? permissions.canUpdateGroup : false;
// Setting a user's AI budget override updates both the user and the group
// its spend is charged to, so it needs permission on both.
const canUpdateBudgetOverride = canUpdateGroup && sitePermissions.updateUsers;
Comment thread
EhabY marked this conversation as resolved.
const [budgetUser, setBudgetUser] = useState<MemberWithSpend | null>(null);

const aibridgeVisible = Boolean(useFeatureVisibility().aibridge);
Expand Down Expand Up @@ -232,6 +237,7 @@ const GroupMembersPage: FC = () => {
user={budgetUser}
currentGroup={groupData}
effectiveGroupId={budgetUser.spend?.effective_group_id}
canUpdate={canUpdateBudgetOverride}
/>
)}
</div>
Expand Down
53 changes: 51 additions & 2 deletions site/src/pages/GroupsPage/GroupPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,20 @@ import {
MockUserMember,
MockUserOwner,
} from "#/testHelpers/entities";
import { withDashboardProvider } from "#/testHelpers/storybook";
import {
withAuthProvider,
withDashboardProvider,
} from "#/testHelpers/storybook";
import GroupMembersPage from "./GroupMembersPage";
import GroupPage from "./GroupPage";

const meta: Meta<typeof GroupPage> = {
title: "pages/OrganizationGroupsPage/GroupPage",
component: GroupPage,
decorators: [withDashboardProvider],
decorators: [withDashboardProvider, withAuthProvider],
parameters: {
user: MockUserOwner,
permissions: { updateUsers: true },
reactRouter: reactRouterParameters({
location: {
pathParams: {
Expand Down Expand Up @@ -674,6 +679,50 @@ export const OpenAIBudgetForCurrentGroupMember: Story = {
},
};

/** Group admins can read a member's budget without the site user permission. */
export const AIBudgetReadOnlyWithoutUserPermission: Story = {
parameters: {
features: ["aibridge"],
permissions: { updateUsers: false },
queries: [
groupQuery(MockGroupWithoutMembers),
groupMembersQuery({
users: [MockUserOwner],
count: 1,
}),
membersSpendQuery([{ ...mockSpend, user_id: MockUserOwner.id }]),
permissionsQuery({ canUpdateGroup: true }),
{ key: meAISpendKey, data: mockUserAISpend },
{ key: getUserAIBudgetOverrideQueryKey(MockUserOwner.id), data: null },
{
key: getGroupsForUserQueryKey(
MockUserOwner.id,
MockGroupWithoutMembers.organization_id,
),
data: [MockGroup2],
},
{ key: groupAIBudget(MockGroupWithoutMembers.id).queryKey, data: null },
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(document.body);

await userEvent.click(
canvas.getAllByRole("button", { name: "Open menu" })[0],
);
await userEvent.click(
await body.findByRole("menuitem", { name: "Manage AI budget" }),
);
await expect(
await body.findByText(
/To update this limit, contact a Coder administrator\./,
),
).toBeInTheDocument();
await expect(body.queryByRole("checkbox")).not.toBeInTheDocument();
},
};

/** Unresolvable via getGroupById, standing in for another org's group. */
const unresolvedGroupId = "external-org-group";

Expand Down
75 changes: 75 additions & 0 deletions site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const meta: Meta<typeof UserAIBudgetOverrideDialog> = {
onOpenChange: () => undefined,
user: MockUserMember,
currentGroup: MockGroup,
canUpdate: true,
},
};

Expand Down Expand Up @@ -88,6 +89,80 @@ export const WithoutOverride: Story = {
},
};

/** Without permission, an existing override can be read but not removed. */
export const ReadOnlyWithOverride: Story = {
args: { canUpdate: false },
parameters: {
queries: [
{
key: getUserAIBudgetOverrideQueryKey(MockUserMember.id),
data: mockOverride,
},
...groupQueries,
],
},
play: async () => {
const body = within(document.body);
await expect(await body.findByText("$12,000 USD")).toBeInTheDocument();
await expect(
body.getByText(/To update this limit, contact a Coder administrator\./),
).toBeInTheDocument();
await expect(body.queryByRole("checkbox")).not.toBeInTheDocument();
await expect(
body.queryByLabelText("Custom monthly budget"),
).not.toBeInTheDocument();
await expect(
body.queryByRole("button", { name: "Budget assigned to" }),
).not.toBeInTheDocument();
for (const name of ["Update", "Cancel", "Close"]) {
await expect(
body.queryByRole("button", { name }),
).not.toBeInTheDocument();
}
},
};

/** Without permission or an override, the group's budget is shown as-is. */
export const ReadOnlyWithoutOverride: Story = {
args: { canUpdate: false },
parameters: {
queries: [
{ key: getUserAIBudgetOverrideQueryKey(MockUserMember.id), data: null },
...groupQueries,
],
},
play: async () => {
const body = within(document.body);
await expect(await body.findByText("$5,000 USD")).toBeInTheDocument();
await expect(
body.getByText(/To update this limit, contact a Coder administrator\./),
).toBeInTheDocument();
await expect(body.queryByRole("checkbox")).not.toBeInTheDocument();
await expect(
body.queryByRole("button", { name: "Update" }),
).not.toBeInTheDocument();
},
};

/** The assigned group is in another organization, so it can't be named. */
export const OverrideWithUnresolvableGroup: Story = {
parameters: {
queries: [
{
key: getUserAIBudgetOverrideQueryKey(MockUserMember.id),
data: { ...mockOverride, group_id: "another-org-group" },
},
...groupQueries,
],
},
play: async () => {
const body = within(document.body);
const summary = await body.findByText(/charged to/);
await expect(summary).toHaveTextContent("charged to their group.");
await expect(summary).not.toHaveTextContent("group group");
},
};

export const Uncapped: Story = {
parameters: {
queries: [
Expand Down
Loading
Loading