diff --git a/components/dashboard/src/components/BillingAccountSelector.tsx b/components/dashboard/src/components/BillingAccountSelector.tsx index 4e2b6822ecbc22..e17183f2fa0f68 100644 --- a/components/dashboard/src/components/BillingAccountSelector.tsx +++ b/components/dashboard/src/components/BillingAccountSelector.tsx @@ -12,12 +12,14 @@ import { TeamsContext } from "../teams/teams-context"; import { UserContext } from "../user-context"; import SelectableCardSolid from "../components/SelectableCardSolid"; import { ReactComponent as Spinner } from "../icons/Spinner.svg"; +import Alert from "./Alert"; export function BillingAccountSelector(props: { onSelected?: () => void }) { const { user, setUser } = useContext(UserContext); const { teams } = useContext(TeamsContext); const [teamsAvailableForAttribution, setTeamsAvailableForAttribution] = useState(); const [membersByTeam, setMembersByTeam] = useState>({}); + const [errorMessage, setErrorMessage] = useState(); useEffect(() => { if (!teams) { @@ -25,7 +27,7 @@ export function BillingAccountSelector(props: { onSelected?: () => void }) { return; } - // Fetch the liust of teams we can actually attribute to + // Fetch the list of teams we can actually attribute to getGitpodService() .server.listAvailableUsageAttributionIds() .then((attrIds) => { @@ -42,17 +44,22 @@ export function BillingAccountSelector(props: { onSelected?: () => void }) { setTeamsAvailableForAttribution( teamsAvailableForAttribution.sort((a, b) => (a.name > b.name ? 1 : -1)), ); + }) + .catch((error) => { + console.error("Could not get list of available billing accounts.", error); + setErrorMessage(`Could not get list of available billing accounts. ${error?.message || String(error)}`); }); const members: Record = {}; - teams.forEach(async (team) => { - try { - members[team.id] = await getGitpodService().server.getTeamMembers(team.id); - } catch (error) { - console.error("Could not get members of team", team, error); - } - }); - setMembersByTeam(members); + Promise.all( + teams.map(async (team) => { + try { + members[team.id] = await getGitpodService().server.getTeamMembers(team.id); + } catch (error) { + console.warn("Could not get members of team", team, error); + } + }), + ).then(() => setMembersByTeam(members)); }, [teams]); const setUsageAttributionTeam = async (team?: Team) => { @@ -74,6 +81,11 @@ export function BillingAccountSelector(props: { onSelected?: () => void }) { return ( <> + {errorMessage && ( + + {errorMessage} + + )} {teamsAvailableForAttribution === undefined && } {teamsAvailableForAttribution && (
diff --git a/components/dashboard/src/components/UsageBasedBillingConfig.tsx b/components/dashboard/src/components/UsageBasedBillingConfig.tsx index 52c6843ad53e1a..31a7022330f713 100644 --- a/components/dashboard/src/components/UsageBasedBillingConfig.tsx +++ b/components/dashboard/src/components/UsageBasedBillingConfig.tsx @@ -41,7 +41,7 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { const [stripePortalUrl, setStripePortalUrl] = useState(); const [pollStripeSubscriptionTimeout, setPollStripeSubscriptionTimeout] = useState(); const [pendingStripeSubscription, setPendingStripeSubscription] = useState(); - const [billingError, setBillingError] = useState(); + const [errorMessage, setErrorMessage] = useState(); const localStorageKey = `pendingStripeSubscriptionFor${attributionId}`; const now = dayjs().utc(true); @@ -63,7 +63,8 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { setStripeSubscriptionId(subscriptionId); setUsageLimit(limit); } catch (error) { - console.error(error); + console.error("Could not get Stripe subscription details.", error); + setErrorMessage(`Could not get Stripe subscription details. ${error?.message || String(error)}`); } finally { setIsLoadingStripeSubscription(false); } @@ -112,7 +113,7 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { window.localStorage.removeItem(localStorageKey); clearTimeout(pollStripeSubscriptionTimeout!); setPendingStripeSubscription(undefined); - setBillingError(`Could not subscribe to Stripe. ${error?.message || String(error)}`); + setErrorMessage(`Could not subscribe to Stripe. ${error?.message || String(error)}`); } })(); }, [attributionId, location.search]); @@ -130,7 +131,7 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { const pending = JSON.parse(pendingStripeSubscription); setPendingStripeSubscription(pending); } catch (error) { - console.error("Could not load pending Stripe subscription", attributionId, error); + console.warn("Could not load pending Stripe subscription", attributionId, error); } }, [attributionId, localStorageKey]); @@ -201,8 +202,8 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { await getGitpodService().server.setUsageLimit(attributionId, newLimit); setUsageLimit(newLimit); } catch (error) { - console.error(error); - setBillingError(`Failed to update usage limit. ${error?.message || String(error)}`); + console.error("Failed to update usage limit", error); + setErrorMessage(`Failed to update usage limit. ${error?.message || String(error)}`); } }; @@ -210,9 +211,9 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {

Manage usage-based billing, usage limit, and payment method.

- {billingError && ( + {errorMessage && ( - {billingError} + {errorMessage} )} {showSpinner && ( @@ -480,7 +481,7 @@ function CreditCardInputForm(props: { attributionId: string }) { const elements = useElements(); const { currency, setCurrency } = useContext(PaymentContext); const [isLoading, setIsLoading] = useState(false); - const [billingError, setBillingError] = useState(); + const [errorMessage, setErrorMessage] = useState(); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); @@ -488,7 +489,7 @@ function CreditCardInputForm(props: { attributionId: string }) { if (!stripe || !elements || !attrId) { return; } - setBillingError(undefined); + setErrorMessage(undefined); setIsLoading(true); try { // Create Stripe customer with currency @@ -508,8 +509,8 @@ function CreditCardInputForm(props: { attributionId: string }) { // site first to authorize the payment, then redirected to the `return_url`. } } catch (error) { - console.error(error); - setBillingError(`Failed to submit form. ${error?.message || String(error)}`); + console.error("Failed to submit form.", error); + setErrorMessage(`Failed to submit form. ${error?.message || String(error)}`); } finally { setIsLoading(false); } @@ -517,9 +518,9 @@ function CreditCardInputForm(props: { attributionId: string }) { return (
- {billingError && ( + {errorMessage && ( - {billingError} + {errorMessage} )} @@ -557,7 +558,7 @@ function UpdateLimitModal(props: { onClose: () => void; onUpdate: (newLimit: number) => {}; }) { - const [error, setError] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); const [newLimit, setNewLimit] = useState( typeof props.currentValue === "number" ? String(props.currentValue) : undefined, ); @@ -565,16 +566,16 @@ function UpdateLimitModal(props: { function onSubmit(event: React.FormEvent) { event.preventDefault(); if (!newLimit) { - setError("Please specify a limit"); + setErrorMessage("Please specify a limit"); return; } const n = parseInt(newLimit, 10); if (typeof n !== "number") { - setError("Please specify a limit that is a valid number"); + setErrorMessage("Please specify a limit that is a valid number"); return; } if (typeof props.minValue === "number" && n < props.minValue) { - setError(`Please specify a limit that is >= ${props.minValue}`); + setErrorMessage(`Please specify a limit that is >= ${props.minValue}`); return; } props.onUpdate(n); @@ -586,9 +587,9 @@ function UpdateLimitModal(props: {

Set usage limit in total credits per month.

- {error && ( + {errorMessage && ( - {error} + {errorMessage} )}