team settings soft budget and alerting emails

This commit is contained in:
yuneng-jiang 2026-02-06 20:40:52 -08:00
parent 51af66fdb2
commit e968e3798c
4 changed files with 105 additions and 4 deletions

View File

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"dev": "next dev --webpack",
"build": "next build",
"start": "next start",
"lint": "next lint",

View File

@ -573,4 +573,63 @@ describe("TeamInfoView", () => {
expect(teamNameElements.length).toBeGreaterThan(0);
});
});
it("should display soft budget in settings view when present", async () => {
const user = userEvent.setup();
vi.mocked(networking.teamInfoCall).mockResolvedValue(
createMockTeamData({
soft_budget: 500.75,
max_budget: 1000,
})
);
renderWithProviders(<TeamInfoView {...defaultProps} />);
await waitFor(() => {
const teamNameElements = screen.queryAllByText("Test Team");
expect(teamNameElements.length).toBeGreaterThan(0);
});
const settingsTab = screen.getByRole("tab", { name: "Settings" });
await user.click(settingsTab);
await waitFor(() => {
expect(screen.getByText("Team Settings")).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByText(/Soft Budget:/)).toBeInTheDocument();
expect(screen.getByText(/\$500\.75/)).toBeInTheDocument();
});
});
it("should display soft budget alerting emails in settings view when present", async () => {
const user = userEvent.setup();
vi.mocked(networking.teamInfoCall).mockResolvedValue(
createMockTeamData({
metadata: {
soft_budget_alerting_emails: ["alert1@test.com", "alert2@test.com"],
},
})
);
renderWithProviders(<TeamInfoView {...defaultProps} />);
await waitFor(() => {
const teamNameElements = screen.queryAllByText("Test Team");
expect(teamNameElements.length).toBeGreaterThan(0);
});
const settingsTab = screen.getByRole("tab", { name: "Settings" });
await user.click(settingsTab);
await waitFor(() => {
expect(screen.getByText("Team Settings")).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByText(/Soft Budget Alerting Emails:/)).toBeInTheDocument();
expect(screen.getByText(/alert1@test\.com, alert2@test\.com/)).toBeInTheDocument();
});
});
});

View File

@ -85,6 +85,7 @@ export interface TeamData {
tpm_limit: number | null;
rpm_limit: number | null;
max_budget: number | null;
soft_budget?: number | null;
budget_duration: string | null;
models: string[];
blocked: boolean;
@ -424,7 +425,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
let parsedMetadata = {};
try {
parsedMetadata = values.metadata ? JSON.parse(values.metadata) : {};
const rawMetadata = values.metadata ? JSON.parse(values.metadata) : {};
// Exclude soft_budget_alerting_emails from parsed metadata since it's handled separately
const { soft_budget_alerting_emails, ...rest } = rawMetadata;
parsedMetadata = rest;
} catch (e) {
NotificationsManager.fromBackend("Invalid JSON in metadata field");
return;
@ -457,12 +461,20 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
tpm_limit: sanitizeNumeric(values.tpm_limit),
rpm_limit: sanitizeNumeric(values.rpm_limit),
max_budget: values.max_budget,
soft_budget: sanitizeNumeric(values.soft_budget),
budget_duration: values.budget_duration,
metadata: {
...parsedMetadata,
guardrails: values.guardrails || [],
logging: values.logging_settings || [],
disable_global_guardrails: values.disable_global_guardrails || false,
soft_budget_alerting_emails:
typeof values.soft_budget_alerting_emails === "string"
? values.soft_budget_alerting_emails
.split(",")
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
: values.soft_budget_alerting_emails || [],
...(secretManagerSettings !== undefined ? { secret_manager_settings: secretManagerSettings } : {}),
},
policies: values.policies || [],
@ -754,6 +766,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
tpm_limit: info.tpm_limit,
rpm_limit: info.rpm_limit,
max_budget: info.max_budget,
soft_budget: info.soft_budget,
budget_duration: info.budget_duration,
team_member_tpm_limit: info.team_member_budget_table?.tpm_limit,
team_member_rpm_limit: info.team_member_budget_table?.rpm_limit,
@ -762,9 +775,13 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
guardrails: info.metadata?.guardrails || [],
policies: info.policies || [],
disable_global_guardrails: info.metadata?.disable_global_guardrails || false,
soft_budget_alerting_emails:
Array.isArray(info.metadata?.soft_budget_alerting_emails)
? info.metadata.soft_budget_alerting_emails.join(", ")
: "",
metadata: info.metadata
? JSON.stringify(
(({ logging, secret_manager_settings, ...rest }) => rest)(info.metadata),
(({ logging, secret_manager_settings, soft_budget_alerting_emails, ...rest }) => rest)(info.metadata),
null,
2,
)
@ -821,6 +838,18 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} />
</Form.Item>
<Form.Item label="Soft Budget (USD)" name="soft_budget">
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} />
</Form.Item>
<Form.Item
label="Soft Budget Alerting Emails"
name="soft_budget_alerting_emails"
tooltip="Comma-separated email addresses to receive alerts when the soft budget is reached"
>
<Input placeholder="example1@test.com, example2@test.com" />
</Form.Item>
<Form.Item
label="Team Member Budget (USD)"
name="team_member_budget"
@ -1096,7 +1125,20 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
Max Budget:{" "}
{info.max_budget !== null ? `$${formatNumberWithCommas(info.max_budget, 4)}` : "No Limit"}
</div>
<div>
Soft Budget:{" "}
{info.soft_budget !== null && info.soft_budget !== undefined
? `$${formatNumberWithCommas(info.soft_budget, 4)}`
: "No Limit"}
</div>
<div>Budget Reset: {info.budget_duration || "Never"}</div>
{info.metadata?.soft_budget_alerting_emails &&
Array.isArray(info.metadata.soft_budget_alerting_emails) &&
info.metadata.soft_budget_alerting_emails.length > 0 && (
<div>
Soft Budget Alerting Emails: {info.metadata.soft_budget_alerting_emails.join(", ")}
</div>
)}
</div>
<div>
<Text className="font-medium">

View File

@ -14,7 +14,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{