mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: remove AI add-on badge, seat column, and add-on wording leftovers (#28004)
Removes all "AI add-on" labeling from the UI, plus two leftover "AI
Governance add-on" wording items from the licensing repackaging (AI
Governance is now included with Premium).
- Remove the **AI add-on** badge from the group settings AI budget
section and the AI Governance add-on card on the Licenses page.
- Remove the **AI add-on** column (check icon showing AI seat
consumption) from the Users and Organization Members tables, along with
its help popover, the `AISeatCell` component, and the
`shouldShowAISeatColumn` entitlement helper.
- Update related Storybook stories.
- Drop "add-on" from the AI cost control route comments in
`enterprise/coderd/coderd.go` and the `coder exp ai-model-prices`
prerequisite in `docs/ai-coder/ai-gateway/cost-controls.md` (leftovers
not covered by #28075/#28077/#28268).
The `has_ai_seat` API field is left intact.
<details>
<summary>Removal scope</summary>
Badge usages removed:
- `site/src/pages/GroupsPage/GroupSettingsPageView.tsx`
-
`site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/AIGovernanceAddOnCard.tsx`
AI seat column plumbing removed:
- `site/src/pages/UsersPage/UsersTable.tsx` and `UsersPage.tsx`
- `site/src/pages/OrganizationSettingsPage/OrganizationMembersTable.tsx`
and `OrganizationMembersPage.tsx`
- `site/src/modules/users/AISeatCell.tsx` (deleted)
- `AiAddonHelpPopover` in `site/src/modules/users/UserHelpPopovers.tsx`
- `shouldShowAISeatColumn` in
`site/src/modules/dashboard/entitlements.ts`
Add-on wording leftovers:
- `enterprise/coderd/coderd.go`: 7 route comments (comments only, no
behavior change)
- `docs/ai-coder/ai-gateway/cost-controls.md`: model prices CLI
prerequisite
</details>
---
🤖 This pull request was generated by Coder Agents on behalf of
@tracyjohnsonux.
This commit is contained in:
@@ -217,9 +217,9 @@ price for it yourself.
|
||||
### Set model prices
|
||||
|
||||
Use the experimental `coder exp ai-model-prices` command to set prices for
|
||||
models the price book does not cover. It requires the AI Governance add-on and
|
||||
the `ai_model_price:update` permission. Run
|
||||
`coder exp ai-model-prices --help` for the full reference.
|
||||
models the price book does not cover. It requires AI Governance, which is
|
||||
included with a Premium license, and the `ai_model_price:update` permission.
|
||||
Run `coder exp ai-model-prices --help` for the full reference.
|
||||
|
||||
List the prices this deployment holds, optionally narrowed to one provider or
|
||||
model:
|
||||
|
||||
@@ -517,7 +517,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Post("/", api.postGroupByOrganization)
|
||||
r.Get("/", api.groupsByOrganization)
|
||||
r.Route("/ai/spend", func(r chi.Router) {
|
||||
// AI cost controls are a paid feature (AI Governance add-on).
|
||||
// AI cost controls are a paid feature (AI Governance).
|
||||
r.Use(
|
||||
api.RequireFeatureMW(codersdk.FeatureAIBridge),
|
||||
)
|
||||
@@ -531,7 +531,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Get("/", api.groupByOrganization)
|
||||
r.Get("/members", api.groupMembersByOrganization)
|
||||
r.Route("/members/ai/spend", func(r chi.Router) {
|
||||
// AI cost controls are a paid feature (AI Governance add-on).
|
||||
// AI cost controls are a paid feature (AI Governance).
|
||||
r.Use(
|
||||
api.RequireFeatureMW(codersdk.FeatureAIBridge),
|
||||
)
|
||||
@@ -548,7 +548,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Get("/", api.paginatedGroups)
|
||||
})
|
||||
r.Route("/organizations/{organization}/ai/spend", func(r chi.Router) {
|
||||
// AI cost controls are a paid feature (AI Governance add-on).
|
||||
// AI cost controls are a paid feature (AI Governance).
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
httpmw.ExtractOrganizationParam(api.Database),
|
||||
@@ -640,21 +640,21 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Delete("/", api.deleteGroup)
|
||||
r.Get("/members", api.groupMembers)
|
||||
r.Route("/members/ai/spend", func(r chi.Router) {
|
||||
// AI cost controls are a paid feature (AI Governance add-on).
|
||||
// AI cost controls are a paid feature (AI Governance).
|
||||
r.Use(
|
||||
api.RequireFeatureMW(codersdk.FeatureAIBridge),
|
||||
)
|
||||
r.Get("/", api.groupMembersAISpend)
|
||||
})
|
||||
r.Route("/ai/spend", func(r chi.Router) {
|
||||
// AI cost controls are a paid feature (AI Governance add-on).
|
||||
// AI cost controls are a paid feature (AI Governance).
|
||||
r.Use(
|
||||
api.RequireFeatureMW(codersdk.FeatureAIBridge),
|
||||
)
|
||||
r.Get("/", api.groupAISpend)
|
||||
})
|
||||
r.Route("/ai/budget", func(r chi.Router) {
|
||||
// AI cost controls are a paid feature (AI Governance add-on).
|
||||
// AI cost controls are a paid feature (AI Governance).
|
||||
r.Use(api.RequireFeatureMW(codersdk.FeatureAIBridge))
|
||||
r.Get("/", api.groupAIBudget)
|
||||
r.Put("/", api.upsertGroupAIBudget)
|
||||
@@ -701,7 +701,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Put("/", api.putUserQuietHoursSchedule)
|
||||
})
|
||||
r.Route("/users/{user}/ai", func(r chi.Router) {
|
||||
// AI cost controls are a paid feature (AI Governance add-on).
|
||||
// AI cost controls are a paid feature (AI Governance).
|
||||
r.Use(
|
||||
api.RequireFeatureMW(codersdk.FeatureAIBridge),
|
||||
apiKeyMiddleware,
|
||||
|
||||
@@ -28,17 +28,3 @@ export const selectFeatureVisibility = (
|
||||
): Record<FeatureName, boolean> => {
|
||||
return getFeatureVisibility(entitlements.has_license, entitlements.features);
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep the AI seats column visible while in grace period so admins can
|
||||
* identify who is consuming seats while remediating overages.
|
||||
*/
|
||||
export const shouldShowAISeatColumn = (entitlements: Entitlements): boolean => {
|
||||
const aiGovernanceUserLimit = entitlements.features.ai_governance_user_limit;
|
||||
return (
|
||||
entitlements.has_license &&
|
||||
aiGovernanceUserLimit.enabled &&
|
||||
(aiGovernanceUserLimit.entitlement === "entitled" ||
|
||||
aiGovernanceUserLimit.entitlement === "grace_period")
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { TableCell } from "#/components/Table/Table";
|
||||
|
||||
interface AISeatCellProps {
|
||||
hasAISeat: boolean;
|
||||
}
|
||||
|
||||
export const AISeatCell: FC<AISeatCellProps> = ({ hasAISeat }) => {
|
||||
return (
|
||||
<TableCell>
|
||||
{hasAISeat ? (
|
||||
<CheckIcon
|
||||
className="size-5 text-content-success"
|
||||
aria-label="Consuming AI seat"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
role="img"
|
||||
aria-label="Not consuming AI seat"
|
||||
className="text-content-disabled"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
@@ -49,18 +49,3 @@ export const GroupsHelpPopover: FC = () => {
|
||||
</HelpPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export const AiAddonHelpPopover: FC = () => {
|
||||
return (
|
||||
<HelpPopover>
|
||||
<HelpPopoverIconTrigger size="small" />
|
||||
<HelpPopoverContent>
|
||||
<HelpPopoverTitle>What is the AI add-on?</HelpPopoverTitle>
|
||||
<HelpPopoverText>
|
||||
Users with access to AI features like AI Gateway or Tasks who are
|
||||
actively consuming a seat.
|
||||
</HelpPopoverText>
|
||||
</HelpPopoverContent>
|
||||
</HelpPopover>
|
||||
);
|
||||
};
|
||||
|
||||
+18
-24
@@ -1,6 +1,5 @@
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -32,29 +31,24 @@ export const AIGovernanceAddOnCard: FC<AIGovernanceAddOnCardProps> = ({
|
||||
>
|
||||
<div className="flex items-center gap-1 px-6 py-1.5">
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium text-content-primary">
|
||||
{title}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="AI Governance seat information"
|
||||
className="m-0 inline-flex appearance-none border-0 bg-transparent p-0 text-content-secondary"
|
||||
>
|
||||
<InfoIcon className="size-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Seats consumed by users using AI Governance features.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Badge variant="magenta" size="sm">
|
||||
AI add-on
|
||||
</Badge>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium text-content-primary">
|
||||
{title}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="AI Governance seat information"
|
||||
className="m-0 inline-flex appearance-none border-0 bg-transparent p-0 text-content-secondary"
|
||||
>
|
||||
<InfoIcon className="size-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Seats consumed by users using AI Governance features.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="min-w-[100px] flex-1 pl-8 pr-3">
|
||||
|
||||
@@ -808,6 +808,5 @@ export const EnterpriseDoesNotShowAIGovernanceAddOn: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(canvas.queryByText("Add-ons")).not.toBeInTheDocument();
|
||||
await expect(canvas.queryByText("AI add-on")).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ type Story = StoryObj<typeof GroupSettingsPageView>;
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// Without the AI add-on, the AI budget section is hidden.
|
||||
// Without the aibridge feature, the AI budget section is hidden.
|
||||
await expect(canvas.queryByText("AI budget")).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useFormik } from "formik";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import * as Yup from "yup";
|
||||
import type { Group } from "#/api/typesGenerated";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { IconField } from "#/components/IconField/IconField";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
@@ -135,7 +134,7 @@ const AIBudgetFeedback: FC<AIBudgetFeedbackProps> = ({
|
||||
|
||||
interface UpdateGroupFormProps {
|
||||
group: Group;
|
||||
/** Whether the AI add-on settings are shown (gated by the aibridge feature). */
|
||||
/** Whether the AI budget settings are shown (gated by the aibridge feature). */
|
||||
showAISettings: boolean;
|
||||
/** Per-member AI budget in dollars, or null for unlimited spend. */
|
||||
initialBudgetDollars: number | null;
|
||||
@@ -251,14 +250,9 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
|
||||
|
||||
{showAISettings && (
|
||||
<section className="flex flex-col gap-8 max-w-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="m-0 text-xl font-semibold text-content-primary">
|
||||
AI budget
|
||||
</h2>
|
||||
<Badge variant="purple" size="sm">
|
||||
AI add-on
|
||||
</Badge>
|
||||
</div>
|
||||
<h2 className="m-0 text-xl font-semibold text-content-primary">
|
||||
AI budget
|
||||
</h2>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Label htmlFor={budgetField.id}>Monthly limit per member</Label>
|
||||
@@ -278,7 +272,9 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
|
||||
placeholder="no budget"
|
||||
aria-invalid={budgetField.error}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">USD</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end" className="pr-3">
|
||||
USD
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<AIBudgetFeedback
|
||||
error={budgetField.error}
|
||||
|
||||
@@ -388,7 +388,9 @@ const OverrideForm: FC<OverrideFormProps> = ({
|
||||
budgetInvalid ? `${budgetId}-error` : undefined
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">USD</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end" className="pr-3">
|
||||
USD
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
{budgetInvalid && (
|
||||
<p
|
||||
|
||||
@@ -21,7 +21,6 @@ import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { useFilter } from "#/components/Filter/Filter";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { usePaginatedQuery } from "#/hooks/usePaginatedQuery";
|
||||
import { shouldShowAISeatColumn } from "#/modules/dashboard/entitlements";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
import { useOrganizationSettings } from "#/modules/management/OrganizationSettingsLayout";
|
||||
import { RequirePermission } from "#/modules/permissions/RequirePermission";
|
||||
@@ -38,7 +37,6 @@ const OrganizationMembersPage: FC = () => {
|
||||
const { organization, organizationPermissions } = useOrganizationSettings();
|
||||
const { entitlements } = useDashboard();
|
||||
const searchParamsResult = useSearchParams();
|
||||
const showAISeatColumn = shouldShowAISeatColumn(entitlements);
|
||||
|
||||
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
|
||||
const groupsByUserIdQuery = useQuery(
|
||||
@@ -123,7 +121,6 @@ const OrganizationMembersPage: FC = () => {
|
||||
organizationName={organizationName}
|
||||
membersQuery={membersQuery}
|
||||
members={members}
|
||||
showAISeatColumn={showAISeatColumn}
|
||||
addMembers={async (users: User[]) => {
|
||||
// TODO: Replace with a batch endpoint (POST /organizations/{org}/members)
|
||||
// to add all users in a single request instead of N individual calls.
|
||||
|
||||
@@ -54,12 +54,6 @@ type Story = StoryObj<typeof OrganizationMembersPageView>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithAIAddonColumn: Story = {
|
||||
args: {
|
||||
showAISeatColumn: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
members: undefined,
|
||||
|
||||
@@ -25,10 +25,8 @@ import {
|
||||
} from "#/components/Table/Table";
|
||||
import { TableEmpty } from "#/components/TableEmpty/TableEmpty";
|
||||
import { TableLoader } from "#/components/TableLoader/TableLoader";
|
||||
import { AISeatCell } from "#/modules/users/AISeatCell";
|
||||
import { UserGroupsCell } from "#/modules/users/UserGroupsCell";
|
||||
import {
|
||||
AiAddonHelpPopover,
|
||||
GroupsHelpPopover,
|
||||
RolesHelpPopover,
|
||||
} from "#/modules/users/UserHelpPopovers";
|
||||
@@ -38,7 +36,6 @@ export type OrganizationMembersTableProps = {
|
||||
// State
|
||||
organizationName: string;
|
||||
members: Array<OrganizationMemberTableEntry> | undefined;
|
||||
showAISeatColumn?: boolean;
|
||||
|
||||
// Actions
|
||||
onEditMemberRoles: (member: OrganizationMemberWithUserData) => void;
|
||||
@@ -62,8 +59,6 @@ type OrganizationMemberTableEntry = OrganizationMemberWithUserData & {
|
||||
export const OrganizationMembersTable: React.FC<
|
||||
OrganizationMembersTableProps
|
||||
> = (props) => {
|
||||
const { showAISeatColumn } = props;
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -81,14 +76,6 @@ export const OrganizationMembersTable: React.FC<
|
||||
<GroupsHelpPopover />
|
||||
</div>
|
||||
</TableHead>
|
||||
{showAISeatColumn && (
|
||||
<TableHead className="w-1/6">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span>AI add-on</span>
|
||||
<AiAddonHelpPopover />
|
||||
</div>
|
||||
</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -101,7 +88,6 @@ export const OrganizationMembersTable: React.FC<
|
||||
const OrganizationMembersTableBody: React.FC<OrganizationMembersTableProps> = ({
|
||||
organizationName,
|
||||
members,
|
||||
showAISeatColumn,
|
||||
|
||||
isUpdatingMemberRoles,
|
||||
removeMember,
|
||||
@@ -141,7 +127,6 @@ const OrganizationMembersTableBody: React.FC<OrganizationMembersTableProps> = ({
|
||||
roles={member.roles}
|
||||
/>
|
||||
<UserGroupsCell userGroups={member.groups} />
|
||||
{showAISeatColumn && <AISeatCell hasAISeat={member.has_ai_seat} />}
|
||||
<TableCell className="w-px whitespace-nowrap text-right">
|
||||
<div className="flex justify-end">
|
||||
{member.user_id !== me && canEditMembers && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, screen, spyOn, userEvent, within } from "storybook/test";
|
||||
import { screen, spyOn, userEvent, within } from "storybook/test";
|
||||
import { API } from "#/api/api";
|
||||
import { deploymentConfigQueryKey } from "#/api/queries/deployment";
|
||||
import { groupsQueryKey } from "#/api/queries/groups";
|
||||
@@ -79,34 +79,6 @@ type Story = StoryObj<typeof UsersPage>;
|
||||
|
||||
export const Loaded: Story = {};
|
||||
|
||||
export const WithAIAddonColumn: Story = {
|
||||
parameters: {
|
||||
features: ["ai_governance_user_limit"],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const header = await canvas.findByRole("columnheader", {
|
||||
name: /AI add-on/i,
|
||||
});
|
||||
|
||||
await expect(header).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutAIAddonColumn: Story = {
|
||||
parameters: {
|
||||
features: ["audit_log"],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByRole("columnheader", { name: "User" });
|
||||
|
||||
await expect(
|
||||
canvas.queryByRole("columnheader", { name: /AI add-on/i }),
|
||||
).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const SuspendUserSuccess: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useFilter } from "#/components/Filter/Filter";
|
||||
import { useStatusFilterMenu } from "#/components/Filter/UsersFilter";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { usePaginatedQuery } from "#/hooks/usePaginatedQuery";
|
||||
import { shouldShowAISeatColumn } from "#/modules/dashboard/entitlements";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
import { RoleSelectorDialog } from "#/modules/roles/RoleSelectorDialog";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
@@ -33,7 +32,6 @@ const UsersPage: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { entitlements } = useDashboard();
|
||||
const showAISeatColumn = shouldShowAISeatColumn(entitlements);
|
||||
|
||||
const groupsByUserIdQuery = useQuery(groupsByUserId());
|
||||
|
||||
@@ -113,7 +111,6 @@ const UsersPage: React.FC = () => {
|
||||
}}
|
||||
usersQuery={usersQuery}
|
||||
groupsByUserId={groupsByUserIdQuery.data}
|
||||
showAISeatColumn={showAISeatColumn}
|
||||
onEditUserRoles={setUserToEditRoles}
|
||||
isUpdatingUserRoles={updateUserRolesMutation.isPending}
|
||||
onResetUserPassword={(user) => {
|
||||
|
||||
@@ -35,10 +35,7 @@ const meta: Meta<typeof UsersPageView> = {
|
||||
totalRecords: 2,
|
||||
data: {
|
||||
count: 2,
|
||||
users: [
|
||||
{ ...MockUserOwner, has_ai_seat: false },
|
||||
{ ...MockUserMember, has_ai_seat: false },
|
||||
],
|
||||
users: [MockUserOwner, MockUserMember],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,32 +26,17 @@ type Story = StoryObj<typeof UsersTable>;
|
||||
|
||||
export const Example: Story = {
|
||||
args: {
|
||||
users: [
|
||||
{ ...MockUserOwner, has_ai_seat: false },
|
||||
{ ...MockUserMember, has_ai_seat: false },
|
||||
],
|
||||
users: [MockUserOwner, MockUserMember],
|
||||
canEditUsers: false,
|
||||
groupsByUserId: mockGroupsByUserId,
|
||||
},
|
||||
};
|
||||
|
||||
export const ExampleWithAISeatColumn: Story = {
|
||||
args: {
|
||||
users: [
|
||||
{ ...MockUserOwner, has_ai_seat: true },
|
||||
{ ...MockUserMember, has_ai_seat: false },
|
||||
],
|
||||
canEditUsers: false,
|
||||
groupsByUserId: mockGroupsByUserId,
|
||||
showAISeatColumn: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Editable: Story = {
|
||||
args: {
|
||||
users: [
|
||||
{ ...MockUserOwner, has_ai_seat: false },
|
||||
{ ...MockUserMember, has_ai_seat: false },
|
||||
MockUserOwner,
|
||||
MockUserMember,
|
||||
{
|
||||
...MockUserOwner,
|
||||
username: "John Doe",
|
||||
@@ -63,7 +48,6 @@ export const Editable: Story = {
|
||||
MockAuditorRole,
|
||||
],
|
||||
status: "dormant",
|
||||
has_ai_seat: false,
|
||||
},
|
||||
{
|
||||
...MockUserOwner,
|
||||
@@ -71,7 +55,6 @@ export const Editable: Story = {
|
||||
email: "roger.moore@coder.com",
|
||||
roles: [],
|
||||
status: "suspended",
|
||||
has_ai_seat: false,
|
||||
},
|
||||
{
|
||||
...MockUserOwner,
|
||||
@@ -80,7 +63,6 @@ export const Editable: Story = {
|
||||
roles: [],
|
||||
status: "active",
|
||||
login_type: "oidc",
|
||||
has_ai_seat: false,
|
||||
},
|
||||
],
|
||||
canEditUsers: true,
|
||||
@@ -89,49 +71,6 @@ export const Editable: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const EditableWithAISeatColumn: Story = {
|
||||
args: {
|
||||
users: [
|
||||
{ ...MockUserOwner, has_ai_seat: true },
|
||||
{ ...MockUserMember, has_ai_seat: false },
|
||||
{
|
||||
...MockUserOwner,
|
||||
username: "John Doe",
|
||||
email: "john.doe@coder.com",
|
||||
roles: [
|
||||
MockUserAdminRole,
|
||||
MockTemplateAdminRole,
|
||||
MockMemberRole,
|
||||
MockAuditorRole,
|
||||
],
|
||||
status: "dormant",
|
||||
has_ai_seat: false,
|
||||
},
|
||||
{
|
||||
...MockUserOwner,
|
||||
username: "Roger Moore",
|
||||
email: "roger.moore@coder.com",
|
||||
roles: [],
|
||||
status: "suspended",
|
||||
has_ai_seat: false,
|
||||
},
|
||||
{
|
||||
...MockUserOwner,
|
||||
username: "OIDC User",
|
||||
email: "oidc.user@coder.com",
|
||||
roles: [],
|
||||
status: "active",
|
||||
login_type: "oidc",
|
||||
has_ai_seat: false,
|
||||
},
|
||||
],
|
||||
canEditUsers: true,
|
||||
canViewActivity: true,
|
||||
groupsByUserId: mockGroupsByUserId,
|
||||
showAISeatColumn: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
users: [],
|
||||
|
||||
@@ -30,10 +30,8 @@ import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "#/components/TableLoader/TableLoader";
|
||||
import { AISeatCell } from "#/modules/users/AISeatCell";
|
||||
import { UserGroupsCell } from "#/modules/users/UserGroupsCell";
|
||||
import {
|
||||
AiAddonHelpPopover,
|
||||
GroupsHelpPopover,
|
||||
RolesHelpPopover,
|
||||
} from "#/modules/users/UserHelpPopovers";
|
||||
@@ -47,7 +45,6 @@ export type UsersTableProps = {
|
||||
isLoading: boolean;
|
||||
users: readonly TypesGen.User[] | undefined;
|
||||
groupsByUserId: GroupsByUserId | undefined;
|
||||
showAISeatColumn?: boolean;
|
||||
|
||||
// Actions
|
||||
onEditUserRoles: (user: TypesGen.User) => void;
|
||||
@@ -70,8 +67,6 @@ export type UsersTableProps = {
|
||||
};
|
||||
|
||||
export const UsersTable: React.FC<UsersTableProps> = (props) => {
|
||||
const { showAISeatColumn } = props;
|
||||
|
||||
return (
|
||||
<Table data-testid="users-table">
|
||||
<TableHeader>
|
||||
@@ -89,14 +84,6 @@ export const UsersTable: React.FC<UsersTableProps> = (props) => {
|
||||
<GroupsHelpPopover />
|
||||
</div>
|
||||
</TableHead>
|
||||
{showAISeatColumn && (
|
||||
<TableHead className="w-1/6">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span>AI add-on</span>
|
||||
<AiAddonHelpPopover />
|
||||
</div>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="w-1/6">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -112,7 +99,6 @@ const UsersTableBody: React.FC<UsersTableProps> = ({
|
||||
isLoading,
|
||||
users,
|
||||
groupsByUserId,
|
||||
showAISeatColumn,
|
||||
|
||||
onEditUserRoles,
|
||||
isUpdatingUserRoles,
|
||||
@@ -127,12 +113,7 @@ const UsersTableBody: React.FC<UsersTableProps> = ({
|
||||
oidcRoleSyncEnabled,
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<UsersTableSkeleton
|
||||
showAISeatColumn={showAISeatColumn}
|
||||
canEditUsers={canEditUsers}
|
||||
/>
|
||||
);
|
||||
return <UsersTableSkeleton canEditUsers={canEditUsers} />;
|
||||
}
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
@@ -157,8 +138,6 @@ const UsersTableBody: React.FC<UsersTableProps> = ({
|
||||
|
||||
<UserGroupsCell userGroups={groupsByUserId?.get(user.id)} />
|
||||
|
||||
{showAISeatColumn && <AISeatCell hasAISeat={user.has_ai_seat} />}
|
||||
|
||||
<TableCell
|
||||
className={cn(
|
||||
"capitalize",
|
||||
@@ -261,12 +240,10 @@ const UsersTableBody: React.FC<UsersTableProps> = ({
|
||||
};
|
||||
|
||||
type UsersTableSkeletonProps = {
|
||||
showAISeatColumn?: boolean;
|
||||
canEditUsers: boolean;
|
||||
};
|
||||
|
||||
const UsersTableSkeleton: React.FC<UsersTableSkeletonProps> = ({
|
||||
showAISeatColumn,
|
||||
canEditUsers,
|
||||
}) => {
|
||||
return (
|
||||
@@ -284,12 +261,6 @@ const UsersTableSkeleton: React.FC<UsersTableSkeletonProps> = ({
|
||||
<Skeleton variant="text" width="25%" />
|
||||
</TableCell>
|
||||
|
||||
{showAISeatColumn && (
|
||||
<TableCell>
|
||||
<Skeleton variant="text" width="25%" />
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
<TableCell>
|
||||
<Skeleton variant="text" width="25%" />
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user