Skip to content

Commit be86e5e

Browse files
authored
[dashboard] cache get SupportedWorkspaceClasses (#16708)
1 parent 00b723d commit be86e5e

File tree

5 files changed

+72
-74
lines changed

5 files changed

+72
-74
lines changed

components/dashboard/src/components/SelectWorkspaceClassComponent.tsx

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
*/
66

77
import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class";
8-
import { useCallback, useEffect, useMemo, useState } from "react";
9-
import { getGitpodService } from "../service/service";
8+
import { useCallback, useEffect, useMemo } from "react";
109
import WorkspaceClass from "../icons/WorkspaceClass.svg";
1110
import { DropDown2, DropDown2Element } from "./DropDown2";
11+
import { useWorkspaceClasses } from "../data/workspaces/workspace-classes-query";
1212

1313
interface SelectWorkspaceClassProps {
1414
selectedWorkspaceClass?: string;
@@ -17,36 +17,34 @@ interface SelectWorkspaceClassProps {
1717
}
1818

1919
export default function SelectWorkspaceClassComponent(props: SelectWorkspaceClassProps) {
20-
const [workspaceClasses, setWorkspaceClasses] = useState<SupportedWorkspaceClass[]>();
21-
useEffect(() => {
22-
getGitpodService().server.getSupportedWorkspaceClasses().then(setWorkspaceClasses);
23-
}, []);
24-
const getElements = useMemo(() => {
25-
return () => {
26-
if (!workspaceClasses) {
27-
return [];
28-
}
29-
return [
30-
...workspaceClasses.map(
31-
(c) =>
32-
({
33-
id: c.id,
34-
element: <WorkspaceClassDropDownElement wsClass={c} />,
35-
isSelectable: true,
36-
} as DropDown2Element),
37-
),
38-
];
39-
};
40-
}, [workspaceClasses]);
20+
const workspaceClasses = useWorkspaceClasses();
21+
const elements = useMemo(() => {
22+
if (!workspaceClasses.data) {
23+
return [];
24+
}
25+
return [
26+
...workspaceClasses.data?.map(
27+
(c) =>
28+
({
29+
id: c.id,
30+
element: <WorkspaceClassDropDownElement wsClass={c} />,
31+
isSelectable: true,
32+
} as DropDown2Element),
33+
),
34+
];
35+
}, [workspaceClasses.data]);
4136
useEffect(() => {
42-
if (!workspaceClasses) {
37+
if (!workspaceClasses.data) {
4338
return;
4439
}
4540
// if the selected workspace class is not supported, we set an error and ask the user to pick one
46-
if (props.selectedWorkspaceClass && !workspaceClasses.find((c) => c.id === props.selectedWorkspaceClass)) {
41+
if (
42+
props.selectedWorkspaceClass &&
43+
!workspaceClasses.data?.find((c) => c.id === props.selectedWorkspaceClass)
44+
) {
4745
props.setError?.(`The workspace class '${props.selectedWorkspaceClass}' is not supported.`);
4846
}
49-
}, [workspaceClasses, props.selectedWorkspaceClass, props.setError, props]);
47+
}, [workspaceClasses.data, props.selectedWorkspaceClass, props.setError, props]);
5048
const internalOnSelectionChange = useCallback(
5149
(id: string) => {
5250
props.onSelectionChange(id);
@@ -56,16 +54,16 @@ export default function SelectWorkspaceClassComponent(props: SelectWorkspaceClas
5654
},
5755
[props],
5856
);
59-
const selectedWsClass = useMemo(
60-
() =>
61-
workspaceClasses?.find(
62-
(ws) => ws.id === (props.selectedWorkspaceClass || workspaceClasses.find((ws) => ws.isDefault)?.id),
63-
),
64-
[props.selectedWorkspaceClass, workspaceClasses],
65-
);
57+
const selectedWsClass = useMemo(() => {
58+
if (!workspaceClasses.data) {
59+
return undefined;
60+
}
61+
const defaultClassId = workspaceClasses.data.find((ws) => ws.isDefault)?.id;
62+
return workspaceClasses.data.find((ws) => ws.id === (props.selectedWorkspaceClass || defaultClassId));
63+
}, [props.selectedWorkspaceClass, workspaceClasses.data]);
6664
return (
6765
<DropDown2
68-
getElements={getElements}
66+
getElements={() => elements}
6967
onSelectionChange={internalOnSelectionChange}
7068
searchPlaceholder="Select class"
7169
disableSearch={true}

components/dashboard/src/components/UsageBasedBillingConfig.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,23 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { useState, useContext, useEffect, useCallback, useMemo } from "react";
7+
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
8+
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
9+
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
10+
import { Appearance, loadStripe, Stripe } from "@stripe/stripe-js";
11+
import dayjs from "dayjs";
12+
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
813
import { useLocation } from "react-router";
914
import { Link } from "react-router-dom";
10-
import { Appearance, loadStripe, Stripe } from "@stripe/stripe-js";
11-
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
12-
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
15+
import DropDown from "../components/DropDown";
16+
import Modal from "../components/Modal";
17+
import { useCurrentOrg } from "../data/organizations/orgs-query";
1318
import { ReactComponent as Spinner } from "../icons/Spinner.svg";
1419
import { ReactComponent as Check } from "../images/check-circle.svg";
15-
import { ThemeContext } from "../theme-context";
1620
import { PaymentContext } from "../payment-context";
1721
import { getGitpodService } from "../service/service";
18-
import DropDown from "../components/DropDown";
19-
import Modal from "../components/Modal";
22+
import { ThemeContext } from "../theme-context";
2023
import Alert from "./Alert";
21-
import dayjs from "dayjs";
22-
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
23-
import { publicApiTeamMembersToProtocol, teamsService } from "../service/public-api";
2424
import { Heading2, Subheading } from "./typography/headings";
2525

2626
const BASE_USAGE_LIMIT_FOR_STRIPE_USERS = 1000;
@@ -33,6 +33,7 @@ interface Props {
3333

3434
export default function UsageBasedBillingConfig({ attributionId }: Props) {
3535
const location = useLocation();
36+
const currentOrg = useCurrentOrg().data;
3637
const attrId = attributionId ? AttributionId.parse(attributionId) : undefined;
3738
const [showUpdateLimitModal, setShowUpdateLimitModal] = useState<boolean>(false);
3839
const [showBillingSetupModal, setShowBillingSetupModal] = useState<boolean>(false);
@@ -104,11 +105,8 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {
104105
// Pick a good initial value for the Stripe usage limit (base_limit * team_size)
105106
// FIXME: Should we ask the customer to confirm or edit this default limit?
106107
let limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS;
107-
if (attrId?.kind === "team") {
108-
const members = publicApiTeamMembersToProtocol(
109-
(await teamsService.getTeam({ teamId: attrId.teamId })).team?.members || [],
110-
);
111-
limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS * members.length;
108+
if (attrId?.kind === "team" && currentOrg) {
109+
limit = BASE_USAGE_LIMIT_FOR_STRIPE_USERS * currentOrg.members.length;
112110
}
113111
const newLimit = await getGitpodService().server.subscribeToStripe(attributionId, setupIntentId, limit);
114112
if (newLimit) {
@@ -133,7 +131,7 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {
133131
setErrorMessage(`Could not subscribe to Stripe. ${error?.message || String(error)}`);
134132
}
135133
})();
136-
}, [attributionId, location.pathname, location.search, refreshSubscriptionDetails]);
134+
}, [attrId?.kind, attributionId, currentOrg, location.pathname, location.search, refreshSubscriptionDetails]);
137135

138136
const showSpinner = !attributionId || isLoadingStripeSubscription || !!pendingStripeSubscription;
139137
const showBalance = !showSpinner;

components/dashboard/src/components/UsageView.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ import Spinner from "../icons/Spinner.svg";
2222
import { ReactComponent as UsageIcon } from "../images/usage-default.svg";
2323
import { toRemoteURL } from "../projects/render-utils";
2424
import { WorkspaceType } from "@gitpod/gitpod-protocol";
25-
import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class";
2625
import DatePicker from "react-datepicker";
2726
import "react-datepicker/dist/react-datepicker.css";
2827
import "./react-datepicker.css";
2928
import { useLocation } from "react-router";
3029
import dayjs from "dayjs";
3130
import { Heading1, Heading2, Subheading } from "./typography/headings";
31+
import { useWorkspaceClasses } from "../data/workspaces/workspace-classes-query";
3232

3333
interface UsageViewProps {
3434
attributionId: AttributionId;
@@ -42,7 +42,7 @@ function UsageView({ attributionId }: UsageViewProps) {
4242
const [endDate, setEndDate] = useState(dayjs());
4343
const [totalCreditsUsed, setTotalCreditsUsed] = useState<number>(0);
4444
const [isLoading, setIsLoading] = useState<boolean>(true);
45-
const [supportedClasses, setSupportedClasses] = useState<SupportedWorkspaceClass[]>([]);
45+
const supportedClasses = useWorkspaceClasses();
4646

4747
const location = useLocation();
4848
useEffect(() => {
@@ -55,10 +55,6 @@ function UsageView({ attributionId }: UsageViewProps) {
5555
console.error(e);
5656
}
5757
}
58-
(async () => {
59-
const classes = await getGitpodService().server.getSupportedWorkspaceClasses();
60-
setSupportedClasses(classes);
61-
})();
6258
}, [location]);
6359

6460
const loadPage = useCallback(
@@ -114,7 +110,7 @@ function UsageView({ attributionId }: UsageViewProps) {
114110
};
115111

116112
const getDisplayName = (workspaceClass: string) => {
117-
const workspaceDisplayName = supportedClasses.find((wc) => wc.id === workspaceClass)?.displayName;
113+
const workspaceDisplayName = supportedClasses.data?.find((wc) => wc.id === workspaceClass)?.displayName;
118114
if (!workspaceDisplayName) {
119115
return workspaceClass;
120116
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class";
8+
import { useQuery } from "@tanstack/react-query";
9+
import { getGitpodService } from "../../service/service";
10+
11+
export const useWorkspaceClasses = () => {
12+
return useQuery<SupportedWorkspaceClass[]>({
13+
queryKey: ["workspace-classes"],
14+
queryFn: async () => {
15+
return getGitpodService().server.getSupportedWorkspaceClasses();
16+
},
17+
cacheTime: 1000 * 60 * 60, // 1h
18+
staleTime: 1000 * 60 * 60, // 1h
19+
});
20+
};

components/dashboard/src/user-settings/selectClass.tsx

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getGitpodService } from "../service/service";
99
import { trackEvent } from "../Analytics";
1010
import WorkspaceClass from "../components/WorkspaceClass";
1111
import { SupportedWorkspaceClass } from "@gitpod/gitpod-protocol/lib/workspace-class";
12+
import { useWorkspaceClasses } from "../data/workspaces/workspace-classes-query";
1213

1314
interface SelectWorkspaceClassProps {
1415
workspaceClass?: string;
@@ -17,6 +18,7 @@ interface SelectWorkspaceClassProps {
1718

1819
export default function SelectWorkspaceClass(props: SelectWorkspaceClassProps) {
1920
const [workspaceClass, setWorkspaceClass] = useState<string | undefined>(props.workspaceClass);
21+
const supportedClasses = useWorkspaceClasses();
2022
const actuallySetWorkspaceClass = async (value: string) => {
2123
const previousValue = await props.setWorkspaceClass(value);
2224
if (previousValue !== value) {
@@ -28,25 +30,9 @@ export default function SelectWorkspaceClass(props: SelectWorkspaceClassProps) {
2830
}
2931
};
3032

31-
const [supportedClasses, setSupportedClasses] = useState<SupportedWorkspaceClass[]>([]);
32-
33-
useEffect(() => {
34-
const fetchClasses = async () => {
35-
const classes = await getGitpodService().server.getSupportedWorkspaceClasses();
36-
setSupportedClasses(classes);
37-
38-
if (!workspaceClass) {
39-
setWorkspaceClass(classes.find((c) => c.isDefault)?.id || "");
40-
}
41-
};
42-
43-
fetchClasses().catch(console.error);
44-
// eslint-disable-next-line react-hooks/exhaustive-deps
45-
}, []);
46-
4733
return (
4834
<div className="mt-4 space-x-3 flex">
49-
{supportedClasses.map((c) => {
35+
{supportedClasses.data?.map((c) => {
5036
return (
5137
<WorkspaceClass
5238
additionalStyles="w-80"

0 commit comments

Comments
 (0)