Skip to content

Commit d34f4cf

Browse files
committed
Refactor entitlements
1 parent 7560d99 commit d34f4cf

File tree

7 files changed

+88
-31
lines changed

7 files changed

+88
-31
lines changed

site/src/api/queries/entitlements.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2+
import * as API from "api/api";
3+
4+
const ENTITLEMENTS_QUERY_KEY = ["entitlements"];
5+
6+
export const useEntitlements = () => {
7+
return useQuery({
8+
queryKey: ENTITLEMENTS_QUERY_KEY,
9+
queryFn: fetchEntitlements,
10+
});
11+
};
12+
13+
export const useRefreshEntitlements = ({
14+
onSuccess,
15+
onError,
16+
}: {
17+
onSuccess: () => void;
18+
onError: (error: unknown) => void;
19+
}) => {
20+
const queryClient = useQueryClient();
21+
return useMutation({
22+
mutationFn: API.refreshEntitlements,
23+
onSuccess: async () => {
24+
await queryClient.invalidateQueries({
25+
queryKey: ENTITLEMENTS_QUERY_KEY,
26+
});
27+
onSuccess();
28+
},
29+
onError,
30+
});
31+
};
32+
33+
const fetchEntitlements = () => {
34+
// Entitlements is injected by the Coder server into the HTML document.
35+
const entitlements = document.querySelector("meta[property=entitlements]");
36+
if (entitlements) {
37+
const rawContent = entitlements.getAttribute("content");
38+
try {
39+
return JSON.parse(rawContent as string);
40+
} catch (e) {
41+
console.warn("Failed to parse entitlements from document", e);
42+
}
43+
}
44+
45+
return API.getEntitlements();
46+
};

site/src/components/Dashboard/DashboardProvider.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useMachine } from "@xstate/react";
22
import { useBuildInfo } from "api/queries/buildInfo";
3+
import { useEntitlements } from "api/queries/entitlements";
34
import {
45
AppearanceConfig,
56
BuildInfoResponse,
@@ -9,7 +10,6 @@ import {
910
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
1011
import { createContext, FC, PropsWithChildren, useContext } from "react";
1112
import { appearanceMachine } from "xServices/appearance/appearanceXService";
12-
import { entitlementsMachine } from "xServices/entitlements/entitlementsXService";
1313
import { experimentsMachine } from "xServices/experiments/experimentsMachine";
1414

1515
interface Appearance {
@@ -32,14 +32,16 @@ export const DashboardProviderContext = createContext<
3232

3333
export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
3434
const buildInfoQuery = useBuildInfo();
35-
const [entitlementsState] = useMachine(entitlementsMachine);
35+
const entitlementsQuery = useEntitlements();
3636
const [appearanceState, appearanceSend] = useMachine(appearanceMachine);
3737
const [experimentsState] = useMachine(experimentsMachine);
38-
const { entitlements } = entitlementsState.context;
3938
const { appearance, preview } = appearanceState.context;
4039
const { experiments } = experimentsState.context;
4140
const isLoading =
42-
!buildInfoQuery.data || !entitlements || !appearance || !experiments;
41+
!buildInfoQuery.data ||
42+
!entitlementsQuery.data ||
43+
!appearance ||
44+
!experiments;
4345

4446
const setAppearancePreview = (config: AppearanceConfig) => {
4547
appearanceSend({
@@ -63,7 +65,7 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
6365
<DashboardProviderContext.Provider
6466
value={{
6567
buildInfo: buildInfoQuery.data,
66-
entitlements,
68+
entitlements: entitlementsQuery.data,
6769
experiments,
6870
appearance: {
6971
preview,

site/src/hooks/useFeatureVisibility.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FeatureName } from "api/typesGenerated";
22
import { useDashboard } from "components/Dashboard/DashboardProvider";
3-
import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors";
3+
import { selectFeatureVisibility } from "utils/entitlements";
44

55
export const useFeatureVisibility = (): Record<FeatureName, boolean> => {
66
const { entitlements } = useDashboard();

site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,40 @@
11
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2-
import { useMachine } from "@xstate/react";
32
import { getLicenses, removeLicense } from "api/api";
43
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
54
import { FC, useEffect } from "react";
65
import { Helmet } from "react-helmet-async";
76
import { useSearchParams } from "react-router-dom";
87
import useToggle from "react-use/lib/useToggle";
98
import { pageTitle } from "utils/page";
10-
import { entitlementsMachine } from "xServices/entitlements/entitlementsXService";
119
import LicensesSettingsPageView from "./LicensesSettingsPageView";
1210
import { getErrorMessage } from "api/errors";
11+
import {
12+
useEntitlements,
13+
useRefreshEntitlements,
14+
} from "api/queries/entitlements";
1315

1416
const LicensesSettingsPage: FC = () => {
1517
const queryClient = useQueryClient();
16-
const [entitlementsState, sendEvent] = useMachine(entitlementsMachine);
17-
const { entitlements, getEntitlementsError } = entitlementsState.context;
1818
const [searchParams, setSearchParams] = useSearchParams();
1919
const success = searchParams.get("success");
2020
const [confettiOn, toggleConfettiOn] = useToggle(false);
21-
if (getEntitlementsError) {
22-
displayError(
23-
getErrorMessage(getEntitlementsError, "Failed to fetch entitlements"),
24-
);
25-
}
21+
const entitlements = useEntitlements();
22+
const refreshEntitlements = useRefreshEntitlements({
23+
onSuccess: () => {
24+
displaySuccess("Successfully refreshed licenses");
25+
},
26+
onError: (error) => {
27+
displayError(getErrorMessage(error, "Failed to refresh entitlements"));
28+
},
29+
});
30+
31+
useEffect(() => {
32+
if (entitlements.error) {
33+
displayError(
34+
getErrorMessage(entitlements.error, "Failed to fetch entitlements"),
35+
);
36+
}
37+
}, [entitlements.error]);
2638

2739
const { mutate: removeLicenseApi, isLoading: isRemovingLicense } =
2840
useMutation(removeLicense, {
@@ -59,14 +71,14 @@ const LicensesSettingsPage: FC = () => {
5971
<LicensesSettingsPageView
6072
showConfetti={confettiOn}
6173
isLoading={isLoading}
62-
userLimitActual={entitlements?.features.user_limit.actual}
63-
userLimitLimit={entitlements?.features.user_limit.limit}
74+
isRefreshing={refreshEntitlements.isLoading}
75+
userLimitActual={entitlements.data?.features.user_limit.actual}
76+
userLimitLimit={entitlements.data?.features.user_limit.limit}
6477
licenses={licenses}
6578
isRemovingLicense={isRemovingLicense}
6679
removeLicense={(licenseId: number) => removeLicenseApi(licenseId)}
6780
refreshEntitlements={() => {
68-
const x = sendEvent("REFRESH");
69-
return !x.context.getEntitlementsError;
81+
refreshEntitlements.mutate();
7082
}}
7183
/>
7284
</>

site/src/pages/DeploySettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import Confetti from "react-confetti";
1212
import { Link } from "react-router-dom";
1313
import useWindowSize from "react-use/lib/useWindowSize";
1414
import MuiLink from "@mui/material/Link";
15-
import { displaySuccess } from "components/GlobalSnackbar/utils";
1615
import Tooltip from "@mui/material/Tooltip";
16+
import { LoadingButton } from "components/LoadingButton/LoadingButton";
1717

1818
type Props = {
1919
showConfetti: boolean;
@@ -22,8 +22,9 @@ type Props = {
2222
userLimitLimit?: number;
2323
licenses?: GetLicensesResponse[];
2424
isRemovingLicense: boolean;
25+
isRefreshing: boolean;
2526
removeLicense: (licenseId: number) => void;
26-
refreshEntitlements?: () => boolean;
27+
refreshEntitlements: () => void;
2728
};
2829

2930
const LicensesSettingsPageView: FC<Props> = ({
@@ -33,6 +34,7 @@ const LicensesSettingsPageView: FC<Props> = ({
3334
userLimitLimit,
3435
licenses,
3536
isRemovingLicense,
37+
isRefreshing,
3638
removeLicense,
3739
refreshEntitlements,
3840
}) => {
@@ -69,18 +71,13 @@ const LicensesSettingsPageView: FC<Props> = ({
6971
Add a license
7072
</Button>
7173
<Tooltip title="Refresh license entitlements. This is done automatically every 10 minutes.">
72-
<Button
73-
onClick={() => {
74-
if (refreshEntitlements) {
75-
if (refreshEntitlements()) {
76-
displaySuccess("Successfully refreshed licenses");
77-
}
78-
}
79-
}}
74+
<LoadingButton
75+
loading={isRefreshing}
76+
onClick={refreshEntitlements}
8077
startIcon={<RefreshIcon />}
8178
>
8279
Refresh
83-
</Button>
80+
</LoadingButton>
8481
</Tooltip>
8582
</Stack>
8683
</Stack>

site/src/xServices/entitlements/entitlementsSelectors.test.ts renamed to site/src/utils/entitlements.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getFeatureVisibility } from "./entitlementsSelectors";
1+
import { getFeatureVisibility } from "./entitlements";
22

33
describe("getFeatureVisibility", () => {
44
it("returns empty object if there is no license", () => {

0 commit comments

Comments
 (0)