diff --git a/src/App.tsx b/src/App.tsx index a4f1edd..716ad2f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,6 +42,7 @@ import "./App.css"; const AUTO_WARMUP_CHECK_INTERVAL_MS = 30 * 1000; const AUTO_WARMUP_RETRY_BACKOFF_MS = 60 * 1000; const LIMIT_FULL_THRESHOLD = 99.5; +const ACCOUNT_SEARCH_THRESHOLD = 8; const SWITCH_ACCOUNT_BLOCKED_EVENT = "switch-account-blocked"; const CLOSE_BEHAVIOR_REQUESTED_EVENT = "close-behavior-requested"; interface SwitchAccountBlockedPayload { @@ -147,6 +148,18 @@ function getTimedWarmupTargets(accounts: AccountWithUsage[]): AccountWithUsage[] ); } +function matchesAccountSearch( + account: AccountWithUsage, + normalizedQuery: string +): boolean { + if (!normalizedQuery) return true; + + return ( + account.name.toLowerCase().includes(normalizedQuery) || + account.email?.toLowerCase().includes(normalizedQuery) === true + ); +} + function App() { const { accounts, @@ -216,6 +229,8 @@ function App() { const [timedWarmupRunning, setTimedWarmupRunning] = useState(false); const [timedWarmupDraft, setTimedWarmupDraft] = useState(""); const [maskedAccounts, setMaskedAccounts] = useState>(new Set()); + const [accountSearchQuery, setAccountSearchQuery] = useState(""); + const isAccountSearchEnabled = accounts.length >= ACCOUNT_SEARCH_THRESHOLD; const [otherAccountsSort, setOtherAccountsSort] = useState< | "deadline_asc" | "deadline_desc" @@ -246,6 +261,12 @@ function App() { accountsRef.current = accounts; }, [accounts]); + useEffect(() => { + if (!isAccountSearchEnabled && accountSearchQuery) { + setAccountSearchQuery(""); + } + }, [accountSearchQuery, isAccountSearchEnabled]); + useEffect(() => { autoWarmupAccountIdsRef.current = autoWarmupAccountIds; }, [autoWarmupAccountIds]); @@ -1203,6 +1224,24 @@ function App() { }); }, [otherAccounts, otherAccountsSort]); + const normalizedAccountSearchQuery = isAccountSearchEnabled + ? accountSearchQuery.trim().toLowerCase() + : ""; + const hasMatchingActiveAccount = + activeAccount !== undefined && + matchesAccountSearch(activeAccount, normalizedAccountSearchQuery); + const visibleOtherAccounts = useMemo( + () => + sortedOtherAccounts.filter((account) => + matchesAccountSearch(account, normalizedAccountSearchQuery) + ), + [normalizedAccountSearchQuery, sortedOtherAccounts] + ); + const hasNoMatchingAccounts = + normalizedAccountSearchQuery.length > 0 && + !hasMatchingActiveAccount && + visibleOtherAccounts.length === 0; + return (
@@ -1543,52 +1582,113 @@ function App() {
) : (
- {/* Active Account */} - {activeAccount && ( -
-

- Active Account -

- { }} - onWarmup={() => - handleWarmupAccount(activeAccount.id, activeAccount.name) - } - onDelete={() => handleDelete(activeAccount.id)} - onRefresh={() => - refreshSingleUsage(activeAccount.id, { refreshMetadata: true }) - } - onRename={(newName) => renameAccount(activeAccount.id, newName)} - switching={switchingId === activeAccount.id} - switchDisabled={hasRunningProcesses ?? false} - warmingUp={ - isWarmingAll || - warmingUpId === activeAccount.id || - autoWarmupRunningIds.has(activeAccount.id) - } - masked={maskedAccounts.has(activeAccount.id)} - onToggleMask={() => toggleMask(activeAccount.id)} - autoWarmupEnabled={ - autoWarmupAllEnabled || autoWarmupAccountIds.has(activeAccount.id) - } - autoWarmupManagedByAll={autoWarmupAllEnabled} - autoWarmupLabel={getAutoWarmupLabel( - activeAccount.usage, - autoWarmupAllEnabled || autoWarmupAccountIds.has(activeAccount.id), - autoWarmupRunningIds.has(activeAccount.id) - )} - onToggleAutoWarmup={() => toggleAutoWarmupAccount(activeAccount.id)} + {isAccountSearchEnabled && ( +
+ + + + setAccountSearchQuery(event.target.value)} + placeholder="Search accounts by name or email" + aria-label="Search accounts" + className="w-full rounded-xl border border-gray-300 bg-white py-2.5 pl-10 pr-10 text-sm text-gray-900 shadow-sm transition-colors placeholder:text-gray-400 focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500 dark:focus:border-gray-600 dark:focus:ring-gray-800" /> -
+ {accountSearchQuery.length > 0 && ( + + )} +
+ )} + + {hasNoMatchingAccounts && ( +
+

+ No matching accounts +

+

+ Try a different account name or email address. +

+
)} + {/* Active Account */} + {activeAccount && + matchesAccountSearch(activeAccount, normalizedAccountSearchQuery) && ( +
+

+ Active Account +

+ { }} + onWarmup={() => + handleWarmupAccount(activeAccount.id, activeAccount.name) + } + onDelete={() => handleDelete(activeAccount.id)} + onRefresh={() => + refreshSingleUsage(activeAccount.id, { refreshMetadata: true }) + } + onRename={(newName) => renameAccount(activeAccount.id, newName)} + switching={switchingId === activeAccount.id} + switchDisabled={hasRunningProcesses ?? false} + warmingUp={ + isWarmingAll || + warmingUpId === activeAccount.id || + autoWarmupRunningIds.has(activeAccount.id) + } + masked={maskedAccounts.has(activeAccount.id)} + onToggleMask={() => toggleMask(activeAccount.id)} + autoWarmupEnabled={ + autoWarmupAllEnabled || autoWarmupAccountIds.has(activeAccount.id) + } + autoWarmupManagedByAll={autoWarmupAllEnabled} + autoWarmupLabel={getAutoWarmupLabel( + activeAccount.usage, + autoWarmupAllEnabled || autoWarmupAccountIds.has(activeAccount.id), + autoWarmupRunningIds.has(activeAccount.id) + )} + onToggleAutoWarmup={() => toggleAutoWarmupAccount(activeAccount.id)} + /> +
+ )} + {/* Other Accounts */} - {otherAccounts.length > 0 && ( + {visibleOtherAccounts.length > 0 && (

- Other Accounts ({otherAccounts.length}) + Other Accounts ({ + normalizedAccountSearchQuery + ? `${visibleOtherAccounts.length} of ${otherAccounts.length}` + : otherAccounts.length + })

- {sortedOtherAccounts.map((account) => ( + {visibleOtherAccounts.map((account) => (