Skip to content
Draft
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
5 changes: 5 additions & 0 deletions apps/web/src/features/accounts/AccountsPage.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,11 @@
font-weight: 750;
}

.accountPlanBadge[role='button'],
.accountCardPlan[role='button'] {
cursor: pointer;
}

.accountResetCreditsButton {
display: inline-flex;
align-items: center;
Expand Down
161 changes: 142 additions & 19 deletions apps/web/src/features/accounts/AccountsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,13 @@
type DetailTab,
} from '@/features/accounts/model/accountsPagePresentation';
import { buildAccountSubscriptionPresentation } from '@/features/accounts/model/accountSubscriptionPresentation';
import {
ensureCodexSubscriptionFresh,
ensureCodexSubscriptionTargetsFresh,
resolveCodexSubscriptionRefreshAuthIndex,
shouldShowCodexSubscriptionTab,
useCodexSubscriptionStore,
} from '@/features/accounts/model/codexSubscription';
import { resolveAccountQuotaWindowUsageAndForecast } from '@/features/accounts/model/accountQuotaWindowUsagePresentation';
import { formatCompactNumber, formatCompactUsd, formatUsd } from '@/utils/usage';
import {
Expand Down Expand Up @@ -291,6 +298,7 @@
AccountOverviewTab,
AccountProviderTabs,
AccountQuotaTab,
AccountSubscriptionTab,
AccountsBatchDeletePreview,
} from '@/features/accounts/components';
import {
Expand All @@ -316,6 +324,7 @@
import type { AuthFileItem, CodexQuotaState, XaiQuotaState } from '@/types';
import {
fetchCodexResetCredits,
resolveCodexChatgptAccountId,
type CodexResetCreditsData,
} from '@/utils/quota';
import type { AuthJsonInputType } from '@/features/authFiles/sessionAuthConverter';
Expand Down Expand Up @@ -1387,6 +1396,8 @@
const [detailTab, setDetailTab] = useState<DetailTab>(
() => initialWorkspaceUrlState.current.detailTab
);
const [subscriptionRefreshing, setSubscriptionRefreshing] = useState(false);
const subscriptionEntries = useCodexSubscriptionStore((state) => state.entries);
const [providerFilter, setProviderFilter] = useState(
() => initialWorkspaceUrlState.current.providerFilter
);
Expand Down Expand Up @@ -3982,27 +3993,29 @@
return { codexQuotaBySelectionKey, codexHeaderSnapshotBySelectionKey };
}, [files, getDisplayCodexHeaderSnapshot, getDisplayCodexQuota]);

const rows = useMemo(
() =>
buildAccountRows(
files,
baseQuotaStores,
effectiveInspectionResults,
accountQuotaOverrides,
freshAccountInspectionBySelectionKey,
credentialEvidenceBoundariesBySelectionKey,
credentialStatusBoundaries
),
[
accountQuotaOverrides,
baseQuotaStores,
const rows = useMemo(() => {
// #742 sort=remaining bakes subscriptionUntilMs on row build; keep this dep
// so ready/soft_failed store flips rebuild rows instead of forking list vs sort.
void subscriptionEntries;
return buildAccountRows(
files,
credentialEvidenceBoundariesBySelectionKey,
credentialStatusBoundaries,
baseQuotaStores,
effectiveInspectionResults,
accountQuotaOverrides,
freshAccountInspectionBySelectionKey,
]
);
credentialEvidenceBoundariesBySelectionKey,
credentialStatusBoundaries
);
}, [
accountQuotaOverrides,
baseQuotaStores,
files,
credentialEvidenceBoundariesBySelectionKey,
credentialStatusBoundaries,
effectiveInspectionResults,
freshAccountInspectionBySelectionKey,
subscriptionEntries,
]);
const codexStatusBySelectionKey = useMemo(() => {
const statusMap = new Map<string, ReturnType<typeof getAuthFileCodexStatus>>();
rows.forEach((row) => {
Expand Down Expand Up @@ -4360,6 +4373,11 @@
() => rows.find((row) => row.selectionKey === selectedRowKey) ?? null,
[rows, selectedRowKey]
);
useEffect(() => {
ensureCodexSubscriptionTargetsFresh(filteredRows, (row) =>
row.provider === CODEX_CONFIG.type ? getActiveCodexQuota(row.raw) : undefined
);
}, [filteredRows, getActiveCodexQuota, subscriptionEntries]);
const accountHistoryTargets = useMemo(() => buildAccountHistoryTargetEntries(rows), [rows]);
const accountHistoryAutoContextKey = useMemo(
() =>
Expand Down Expand Up @@ -5087,6 +5105,19 @@
]
);

const openAccountSubscriptionDetail = useCallback(
(row: AccountRow, event?: { stopPropagation(): void; preventDefault(): void }) => {
event?.stopPropagation();
event?.preventDefault();
const quota = row.provider === CODEX_CONFIG.type ? getActiveCodexQuota(row.raw) : undefined;
void openAccountDetail(
row,
shouldShowCodexSubscriptionTab(row, quota) ? 'subscription' : 'overview'
);
},
[getActiveCodexQuota, openAccountDetail]
);

const closeAccountDetail = useCallback(() => {
detailAnchorRef.current = null;
setDetailAnchor(null);
Expand Down Expand Up @@ -7044,7 +7075,7 @@
};
void runResetTransaction();
},
[

Check warning on line 7078 in apps/web/src/features/accounts/AccountsPage.tsx

View workflow job for this annotation

GitHub Actions / Frontend

React Hook useCallback has a missing dependency: 'connectionFingerprint'. Either include it or remove the dependency array
canResetCodexQuota,
getDisplayAccount,
invalidateCodexCredentialStatusForSelectionKeys,
Expand Down Expand Up @@ -8459,9 +8490,14 @@
});
const codexQuotaState =
row.provider === CODEX_CONFIG.type ? getActiveCodexQuota(row.raw) : undefined;
const subscriptionAccountId = resolveCodexChatgptAccountId(row.raw);
const subscriptionEntry = subscriptionAccountId
? subscriptionEntries[subscriptionAccountId]
: undefined;
const subscriptionPresentation = buildAccountSubscriptionPresentation({
row,
codexQuota: codexQuotaState,
subscriptionsRecord: subscriptionEntry?.status === 'ready' ? subscriptionEntry.record : null,
});
const codexResetCreditsCount =
codexQuotaState?.rateLimitResetCreditsAvailableCount ??
Expand Down Expand Up @@ -8768,11 +8804,28 @@
? styles.accountPlanBadgeDaysWarning
: styles.accountPlanBadgeDaysNormal
: '';
const planOpenProps = ctx.subscriptionPresentation.isPaidCodex
? {
role: 'button' as const,
tabIndex: 0,
onClick: (event: ReactMouseEvent) =>
openAccountSubscriptionDetail(row, event),
onKeyDown: (event: KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
openAccountSubscriptionDetail(row, event);
}
},
'aria-label': t('accounts.open_subscription_detail', {
name: getDisplayFileName(row.fileName),
}),
}
: {};
return ctx.subscriptionPresentation.planPresentation?.shortLabel &&
ctx.subscriptionPresentation.planPresentation.shortLabel !== '-' ? (
<span
className={styles.accountPlanBadge}
title={ctx.subscriptionPresentation.planPresentation?.fullLabel}
{...planOpenProps}
>
{ctx.subscriptionPresentation.planPresentation.shortLabel}
{remainingDays !== null ? (
Expand All @@ -8794,6 +8847,7 @@
days: remainingDays,
defaultValue: `剩余 ${remainingDays} 天`,
})}
{...planOpenProps}
>
{t('accounts.list_plan_remaining_days', {
days: remainingDays,
Expand Down Expand Up @@ -9233,7 +9287,25 @@
</div>
</div>

<div className={styles.accountCardPlan}>
<div
className={styles.accountCardPlan}
{...(ctx.subscriptionPresentation.isPaidCodex
? {
role: 'button' as const,
tabIndex: 0,
onClick: (event: ReactMouseEvent) =>
openAccountSubscriptionDetail(row, event),
onKeyDown: (event: KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
openAccountSubscriptionDetail(row, event);
}
},
'aria-label': t('accounts.open_subscription_detail', {
name: getDisplayFileName(row.fileName),
}),
}
: {})}
>
<span
className={styles.accountPlanName}
title={ctx.subscriptionPresentation.planPresentation?.fullLabel}
Expand Down Expand Up @@ -9408,9 +9480,22 @@

const providerIcon = getAuthFileIcon(selectedRow.provider, resolvedTheme);

const selectedSubscriptionAccountId = resolveCodexChatgptAccountId(selectedRow.raw);
const selectedSubscriptionEntry = selectedSubscriptionAccountId
? subscriptionEntries[selectedSubscriptionAccountId] ?? { status: 'idle' as const }
: { status: 'idle' as const };
const selectedSubscriptionRecord =
selectedSubscriptionEntry.status === 'ready' ? selectedSubscriptionEntry.record : null;
const showSubscriptionTab = shouldShowCodexSubscriptionTab(
selectedRow,
selectedRow.provider === CODEX_CONFIG.type ? getActiveCodexQuota(selectedRow.raw) : undefined
);
const detailTabs: Array<{ id: DetailTab; label: string }> = [
{ id: 'overview', label: t('accounts.detail_tab_overview') },
{ id: 'quota', label: t('accounts.detail_tab_quota') },
...(showSubscriptionTab
? [{ id: 'subscription' as const, label: t('accounts.detail_tab_subscription') }]
: []),
{ id: 'config', label: t('accounts.detail_tab_config') },
{ id: 'models', label: t('accounts.detail_tab_models') },
{ id: 'diagnostics', label: t('accounts.detail_tab_diagnostics') },
Expand Down Expand Up @@ -9482,6 +9567,7 @@
history: accountHistoryByRowKey.get(selectedRow.selectionKey) ?? null,
valueRow,
codexQuota: selectedCodexQuota,
codexSubscription: selectedSubscriptionRecord,
xaiQuota:
selectedRow.provider === XAI_CONFIG.type
? getCredentialScopedQuotaState(xaiQuota, selectedRow.raw)
Expand All @@ -9498,6 +9584,43 @@
const modelsTargetMatches = modelsSelectionKey === selectedRow.selectionKey;

const renderActiveDetail = () => {
if (detailTab === 'subscription' && showSubscriptionTab) {
return (
<AccountSubscriptionTab
entry={selectedSubscriptionEntry}
missingAccountId={!selectedSubscriptionAccountId}
refreshing={subscriptionRefreshing || selectedSubscriptionEntry.status === 'loading'}
onRefresh={() => {
if (!selectedSubscriptionAccountId) return;
const siblingAuthIndexes = [
...new Set(
filteredRows
.filter(
(row) =>
resolveCodexChatgptAccountId(row.raw) === selectedSubscriptionAccountId
)
.map((row) =>
normalizeAuthIndex(row.authIndex ?? row.raw.auth_index ?? row.raw.authIndex)
)
.filter((index): index is string => Boolean(index))
),
];
const authIndex = resolveCodexSubscriptionRefreshAuthIndex(
selectedSubscriptionAccountId,
selectedRow.authIndex ?? '',
siblingAuthIndexes
);
if (!authIndex) return;
setSubscriptionRefreshing(true);
void ensureCodexSubscriptionFresh({
accountId: selectedSubscriptionAccountId,
authIndex,
force: true,
}).finally(() => setSubscriptionRefreshing(false));
}}
/>
);
}
if (detailTab === 'quota') {
return (
<AccountQuotaTab
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ describe('accounts workspace wiring', () => {
locale.accounts.detail_tab_models,
locale.accounts.detail_tab_diagnostics,
]).toEqual(expectedLabels);
expect(locale.accounts.detail_tab_subscription).toBeTypeOf('string');
});

it.each([en, ru, zhCN, zhTW])('keeps credential-configuration copy complete', (locale) => {
Expand Down
Loading
Loading