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
90 changes: 52 additions & 38 deletions dashboard/src/ui/dashboard/components/SubscriptionSettingsCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,17 @@ export function SubscriptionSettingsCard({ subscriptions, onChanged }) {
const [expandedId, setExpandedId] = useState(null);
const [now, setNow] = useState(() => Date.now());

const copyLocale = getCopyLocale();
const dateFormat = useMemo(
() =>
new Intl.DateTimeFormat(getCopyLocale(), {
new Intl.DateTimeFormat(copyLocale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}),
[],
[copyLocale],
);

// Refresh countdowns/remaining labels once a minute.
Expand All @@ -73,7 +74,7 @@ export function SubscriptionSettingsCard({ subscriptions, onChanged }) {
{ value: "", label: copy("subscriptions.form.provider_none") },
...LIMIT_PROVIDER_IDS.map((id) => ({ value: id, label: limitProviderName(id) })),
],
[],
[copyLocale],
);

const cycleOptions = useMemo(
Expand All @@ -82,7 +83,7 @@ export function SubscriptionSettingsCard({ subscriptions, onChanged }) {
{ value: "monthly", label: copy("subscriptions.form.cycle_monthly") },
{ value: "yearly", label: copy("subscriptions.form.cycle_yearly") },
],
[],
[copyLocale],
);

const openAdd = useCallback(() => {
Expand Down Expand Up @@ -168,20 +169,27 @@ export function SubscriptionSettingsCard({ subscriptions, onChanged }) {
const list = subscriptions || [];

return (
<div className="flex w-[min(90vw,22rem)] flex-col gap-3 rounded-xl border border-oai-gray-200 bg-white p-4 shadow-lg dark:border-oai-gray-700 dark:bg-oai-gray-900">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-semibold text-oai-black dark:text-white">
{copy("limits.page.openSubscriptions")}
</span>
<Button type="button" size="sm" onClick={openAdd} className="gap-1.5">
<div className="flex max-h-[min(80vh,42rem)] w-[min(92vw,32rem)] flex-col gap-3 overflow-y-auto rounded-xl border border-oai-gray-200 bg-white p-4 shadow-lg dark:border-oai-gray-700 dark:bg-oai-gray-900">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<span className="block truncate text-sm font-semibold text-oai-black dark:text-white">
{copy("limits.page.openSubscriptions")}
</span>
</div>
<Button type="button" size="sm" onClick={openAdd} className="shrink-0 gap-1.5">
<Plus size={14} strokeWidth={2} aria-hidden />
<span>{copy("subscriptions.add")}</span>
</Button>
</div>

{formOpen ? (
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-3 border-b border-oai-gray-100 pb-3 dark:border-oai-gray-800">
<div className="flex flex-col">
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-3 border-y border-oai-gray-100 py-3 dark:border-oai-gray-800 sm:grid-cols-2">
<div className="flex items-center justify-between sm:col-span-2">
<span className="text-xs font-semibold uppercase tracking-wide text-oai-gray-500 dark:text-oai-gray-400">
{editingId ? copy("subscriptions.edit") : copy("subscriptions.add")}
</span>
</div>
<div className="flex flex-col sm:col-span-1">
<label
htmlFor="subscription-provider"
className="block text-sm font-medium text-oai-gray-700 dark:text-oai-gray-300 mb-1.5"
Expand All @@ -197,7 +205,7 @@ export function SubscriptionSettingsCard({ subscriptions, onChanged }) {
className="h-10 w-full px-3 text-sm"
/>
</div>
<div className="flex flex-col">
<div className="flex flex-col sm:col-span-1">
<label
htmlFor="subscription-cycle"
className="block text-sm font-medium text-oai-gray-700 dark:text-oai-gray-300 mb-1.5"
Expand All @@ -213,33 +221,39 @@ export function SubscriptionSettingsCard({ subscriptions, onChanged }) {
className="h-10 w-full px-3 text-sm"
/>
</div>
<Input
label={copy("subscriptions.form.service")}
value={form.service}
maxLength={120}
required
placeholder={copy("subscriptions.form.service_placeholder")}
onChange={(event) => setForm({ ...form, service: event.target.value })}
/>
<Input
label={copy("subscriptions.form.plan")}
value={form.plan}
maxLength={120}
placeholder={copy("subscriptions.form.plan_placeholder")}
onChange={(event) => setForm({ ...form, plan: event.target.value })}
/>
<Input
label={copy("subscriptions.form.next_billing")}
type="datetime-local"
value={form.nextBillingAt}
required
onChange={(event) => setForm({ ...form, nextBillingAt: event.target.value })}
/>
<div className="flex flex-col">
<div className="sm:col-span-1">
<Input
label={copy("subscriptions.form.service")}
value={form.service}
maxLength={120}
required
placeholder={copy("subscriptions.form.service_placeholder")}
onChange={(event) => setForm({ ...form, service: event.target.value })}
/>
</div>
<div className="sm:col-span-1">
<Input
label={copy("subscriptions.form.plan")}
value={form.plan}
maxLength={120}
placeholder={copy("subscriptions.form.plan_placeholder")}
onChange={(event) => setForm({ ...form, plan: event.target.value })}
/>
</div>
<div className="sm:col-span-2">
<Input
label={copy("subscriptions.form.next_billing")}
type="datetime-local"
value={form.nextBillingAt}
required
onChange={(event) => setForm({ ...form, nextBillingAt: event.target.value })}
/>
</div>
<div className="flex flex-col rounded-lg border border-oai-gray-200/70 bg-oai-gray-50/50 px-3 py-2.5 dark:border-oai-gray-700/60 dark:bg-oai-gray-800/20 sm:col-span-2">
<span className="block text-sm font-medium text-oai-gray-700 dark:text-oai-gray-300 mb-1.5">
{copy("subscriptions.form.auto_renew")}
</span>
<label className="flex h-10 items-center gap-2.5">
<label className="mt-0.5 flex min-h-10 items-center gap-2.5">
<input
type="checkbox"
checked={form.autoRenew}
Expand All @@ -251,7 +265,7 @@ export function SubscriptionSettingsCard({ subscriptions, onChanged }) {
</span>
</label>
</div>
<div className="flex items-center justify-end gap-2">
<div className="flex items-center justify-end gap-2 sm:col-span-2">
{formError ? (
<p className="mr-auto text-sm text-oai-error" role="alert">
{copy("subscriptions.form.error")}
Expand Down
48 changes: 29 additions & 19 deletions dashboard/src/ui/dashboard/components/UsageLimitsPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { ChevronDown } from "lucide-react";
import { Card } from "../../components";
import { FadeIn } from "../../foundation/FadeIn.jsx";
import { copy, getCopyLocale } from "../../../lib/copy";
Expand Down Expand Up @@ -310,19 +311,19 @@ function StatusBadge({ label, age = null, tone = "live", tooltip = null }) {
function ToolGroup({ name, providerId, children, expandable = false, expanded = false, onToggle, badge = null, rightAdornment = null }) {
const providerKey = limitProviderIconKey(providerId);
const header = (
<div className="flex items-center gap-1.5">
<span className="flex min-w-0 flex-1 items-center gap-1.5">
{providerKey ? (
<ProviderIcon provider={providerKey} size={14} className={LIMITS_PROVIDER_ICON_CLASS} />
) : null}
<span className="text-sm font-medium text-oai-black dark:text-oai-white">{name}</span>
<span className="min-w-0 truncate text-sm font-medium text-oai-black dark:text-oai-white">{name}</span>
{badge}
{rightAdornment ? <span className="ml-auto shrink-0">{rightAdornment}</span> : null}
</div>
</span>
);

if (!expandable) {
return (
<div className="flex flex-col gap-1.5">
<div data-limit-group={providerId} className="flex flex-col gap-1.5">
{header}
{children}
</div>
Expand All @@ -331,20 +332,29 @@ function ToolGroup({ name, providerId, children, expandable = false, expanded =

return (
<div
role="button"
tabIndex={0}
aria-expanded={expanded}
onClick={onToggle}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onToggle?.();
}
}}
className="flex flex-col gap-1.5 -mx-1.5 px-1.5 py-1 rounded-lg cursor-pointer transition-colors hover:bg-oai-gray-50 dark:hover:bg-oai-gray-800/40 aria-expanded:bg-oai-gray-50 dark:aria-expanded:bg-oai-gray-800/40"
data-limit-group={providerId}
className={`flex flex-col gap-1.5 -mx-1.5 rounded-xl px-1.5 py-1 transition-colors ${
expanded ? "bg-oai-gray-50/80 dark:bg-oai-gray-800/30" : ""
}`}
>
{header}
{children}
<button
type="button"
aria-expanded={expanded}
aria-controls={`limits-provider-${providerId}`}
onClick={onToggle}
className="group flex w-full items-center gap-1.5 rounded-lg px-1.5 py-1 text-left transition-colors hover:bg-oai-gray-100/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-oai-brand-500 dark:hover:bg-oai-gray-800/60"
>
{header}
<ChevronDown
aria-hidden
className={`h-3.5 w-3.5 shrink-0 text-oai-gray-400 transition-transform duration-200 dark:text-oai-gray-500 ${
expanded ? "rotate-180" : ""
}`}
/>
</button>
<div id={`limits-provider-${providerId}`} className="flex flex-col gap-1.5 px-1.5 pb-1">
{children}
</div>
</div>
);
}
Expand Down Expand Up @@ -381,7 +391,7 @@ function SubscriptionBar({ subscription, now }) {
const labelPct =
widthPct > 0 && rounded === 0 ? copy("limits.bar.sub_one_percent") : `${rounded}%`;
return (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 rounded-lg border border-oai-gray-200/70 bg-oai-gray-50/60 px-2 py-1.5 dark:border-oai-gray-700/60 dark:bg-oai-gray-800/20">
<span
className="text-[11px] text-oai-gray-500 dark:text-oai-gray-400 shrink-0 whitespace-nowrap"
style={{ width: "var(--tt-limits-label-w)" }}
Expand Down Expand Up @@ -421,7 +431,7 @@ function SubscriptionDetail({ subscription, now }) {
minute: "2-digit",
});
return (
<div className="text-[11px] leading-snug text-oai-gray-500 dark:text-oai-gray-400">
<div className="rounded-lg border-l-2 border-oai-brand-300 bg-oai-gray-50/60 px-2.5 py-2 text-[11px] leading-snug text-oai-gray-500 dark:border-oai-brand-700 dark:bg-oai-gray-800/20 dark:text-oai-gray-400">
<span>{copy("subscriptions.inline.label")}</span>
<span className="text-oai-gray-300 dark:text-oai-gray-600">:</span>
<span>
Expand Down
53 changes: 43 additions & 10 deletions dashboard/src/ui/dashboard/components/UsageLimitsPanel.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ describe("UsageLimitsPanel", () => {
/>,
);

const group = screen.getByText("OpenCode Go").closest("[role='button']");
const group = screen.getByText("OpenCode Go").closest("[data-limit-group]");
expect(group).not.toBeNull();
fireEvent.click(group);
fireEvent.click(within(group).getByRole("button", { name: /OpenCode Go/ }));

expect(within(group).queryByText(copy("limits.explain.body"))).not.toBeInTheDocument();
});
Expand All @@ -235,10 +235,10 @@ describe("UsageLimitsPanel", () => {
/>,
);

const group = screen.getByText("Cursor").closest("[role='button']");
const group = screen.getByText("Cursor").closest("[data-limit-group]");
expect(group).not.toBeNull();
expect(group.querySelectorAll("div.absolute.top-0.h-full")).toHaveLength(2);
fireEvent.click(group);
fireEvent.click(within(group).getByRole("button", { name: /Cursor/ }));
expect(within(group).getByText(copy("limits.explain.body"))).toBeInTheDocument();
});

Expand Down Expand Up @@ -429,7 +429,7 @@ describe("UsageLimitsPanel", () => {
/>,
);

const group = screen.getByText("Antigravity").closest("[role='button']");
const group = screen.getByText("Antigravity").closest("[data-limit-group]");
expect(group).not.toBeNull();
expect(within(group).getByText(/cached\s*·/i)).toBeInTheDocument();
expect(group.querySelector("span.bg-amber-500")).not.toBeNull();
Expand Down Expand Up @@ -457,7 +457,7 @@ describe("UsageLimitsPanel", () => {
/>,
);

const group = screen.getByText("Claude").closest("[role='button']");
const group = screen.getByText("Claude").closest("[data-limit-group]");
expect(group).not.toBeNull();
expect(within(group).getByText(new RegExp(copy("limits.reauth.badge")))).toBeInTheDocument();
expect(within(group).queryByText(/^Stale/i)).not.toBeInTheDocument();
Expand Down Expand Up @@ -560,7 +560,7 @@ describe("UsageLimitsPanel", () => {
/>,
);

const codexGroupElement = screen.getByText("Codex").closest("[role='button']");
const codexGroupElement = screen.getByText("Codex").closest("[data-limit-group]");
expect(codexGroupElement).not.toBeNull();
const codexGroup = within(codexGroupElement);
expect(codexGroup.getByText("5h")).toBeInTheDocument();
Expand Down Expand Up @@ -601,7 +601,7 @@ describe("UsageLimitsPanel", () => {
/>,
);

const codexGroupElement = screen.getByText("Codex").closest("[role='button']");
const codexGroupElement = screen.getByText("Codex").closest("[data-limit-group]");
expect(codexGroupElement).not.toBeNull();
const section = codexGroupElement.querySelector("[data-reset-bank-section='count_only']");
expect(section).not.toBeNull();
Expand All @@ -628,7 +628,7 @@ describe("UsageLimitsPanel", () => {
/>,
);

const codexGroupElement = screen.getByText("Codex").closest("[role='button']");
const codexGroupElement = screen.getByText("Codex").closest("[data-limit-group]");
expect(codexGroupElement).not.toBeNull();
expect(codexGroupElement.querySelector("[data-reset-bank-section]")).toBeNull();
expect(within(codexGroupElement).queryByText(copy("limits.codex_reset_bank.title"))).not.toBeInTheDocument();
Expand Down Expand Up @@ -663,11 +663,44 @@ describe("UsageLimitsPanel", () => {
expect(screen.getByText("Subscription")).toBeInTheDocument();

// Expanding reveals the subscription detail line.
fireEvent.click(screen.getByText("Cursor").closest("[role='button']"));
const group = screen.getByText("Cursor").closest("[data-limit-group]");
const toggle = within(group).getByRole("button", { name: /Cursor/ });
expect(toggle).toHaveAttribute("aria-expanded", "false");
expect(toggle).toHaveAttribute("aria-controls", "limits-provider-cursor");
fireEvent.click(toggle);
expect(toggle).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("Next renewal")).toBeInTheDocument();
expect(screen.getByText("Auto-renew on")).toBeInTheDocument();
});

it("keeps provider setup actions outside the expand toggle", () => {
const subscription = {
id: "s1",
service: "OpenCode Go",
provider: "opencodeGo",
autoRenew: true,
nextBillingAt: new Date(Date.now() + 2 * 86400000).toISOString(),
};

render(
<UsageLimitsPanel
opencodeGo={{ configured: false }}
order={["opencodeGo"]}
subscriptions={[subscription]}
/>,
);

const group = screen.getByText("OpenCode Go").closest("[data-limit-group]");
const toggle = within(group).getByRole("button", { name: /OpenCode Go/ });
const copyButton = within(group).getByRole("button", {
name: copy("limits.opencodeGo.setupHint.copy"),
});

expect(toggle).toHaveAttribute("aria-expanded", "false");
fireEvent.click(copyButton);
expect(toggle).toHaveAttribute("aria-expanded", "false");
});

it("does not render subscription rows for a provider without a linked subscription", () => {
render(
<UsageLimitsPanel
Expand Down