feat(site): polish AI budget members table (#26805)

Finishing touches for the AI cost control group members table.

- Spend stays primary-colored until near the budget limit, via a new
  AIBudgetAmount component
- "AI budget period" label shows the current spend window in local
  time, next to the members tab
- Budget tooltip notes the reset date and the group's default limit
- "Budget type" renamed to "Budget group", with a badge for the
  governing group or override
- Distinguishes $0 budget ("None") from no budget ("Unlimited")
- Unattributed spend from another group shows a note instead of a dash
- "Manage AI budget" disabled only when another named group governs
- Replaces UserAISpend with generated UserAISpendStatus, fixing
  limit_source

Closes #26401
This commit is contained in:
Ehab Younes
2026-07-13 17:33:40 +03:00
committed by GitHub
parent 8fee087952
commit d5e5b10ff1
21 changed files with 1060 additions and 506 deletions
+3 -12
View File
@@ -213,7 +213,7 @@ export type GroupMemberAICostControl = Readonly<{
current_spend_micros: number;
spend_limit_micros: number | null;
effective_group_id: string | null;
limit_source: "group" | "override" | null;
limit_source: TypesGen.AIBudgetLimitSource | null;
}>;
export type GroupMemberWithAICostControl = TypesGen.ReducedUser &
Readonly<{ ai_cost_control?: GroupMemberAICostControl }>;
@@ -223,15 +223,6 @@ export type GroupMembersResponseWithAICostControl = Omit<
> &
Readonly<{ users: readonly GroupMemberWithAICostControl[] }>;
// TODO(AIGOV-473): drop once generated from codersdk.
export type UserAISpend = Readonly<{
user_id: string;
spend_limit_micros: number | null;
effective_group_id: string | null;
limit_source: "group" | "override" | null;
current_spend_micros: number;
}>;
export function watchInboxNotifications(
params?: WatchInboxNotificationsParams,
): OneWayWebSocket<TypesGen.GetInboxNotificationResponse> {
@@ -584,8 +575,8 @@ class ApiMethods {
return response.data;
};
getUserAISpend = async (): Promise<UserAISpend> => {
const response = await this.axios.get<UserAISpend>(
getUserAISpend = async (): Promise<TypesGen.UserAISpendStatus> => {
const response = await this.axios.get<TypesGen.UserAISpendStatus>(
"/api/v2/users/me/ai/spend",
);
return response.data;
+3 -2
View File
@@ -4,7 +4,7 @@ import type {
UseMutationOptions,
UseQueryOptions,
} from "react-query";
import { API, type UserAISpend } from "#/api/api";
import { API } from "#/api/api";
import { isApiError } from "#/api/errors";
import type {
AuthorizationRequest,
@@ -19,6 +19,7 @@ import type {
UpsertUserAIBudgetOverrideRequest,
User,
UserAIBudgetOverride,
UserAISpendStatus,
UserAppearanceSettings,
UserPreferenceSettings,
UsersRequest,
@@ -159,7 +160,7 @@ export const me = (metadata: MetadataState<User>) => {
export const meAISpendKey = [...meKey, "aiSpend"] as const;
export const meAISpend = (): UseQueryOptions<UserAISpend> => {
export const meAISpend = (): UseQueryOptions<UserAISpendStatus> => {
return {
queryKey: meAISpendKey,
queryFn: () => API.getUserAISpend(),