Skip to content

Commit 79d4179

Browse files
chore(site): migrate a few services to react-query used in the DashboardProvider (#9667)
1 parent 3b088a5 commit 79d4179

29 files changed

+222
-521
lines changed

site/src/api/queries/appearance.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { QueryClient } from "@tanstack/react-query";
2+
import * as API from "api/api";
3+
import { AppearanceConfig } from "api/typesGenerated";
4+
import { getMetadataAsJSON } from "utils/metadata";
5+
6+
export const appearance = () => {
7+
return {
8+
queryKey: ["appearance"],
9+
queryFn: async () =>
10+
getMetadataAsJSON<AppearanceConfig>("appearance") ?? API.getAppearance(),
11+
};
12+
};
13+
14+
export const updateAppearance = (queryClient: QueryClient) => {
15+
return {
16+
mutationFn: API.updateAppearance,
17+
onSuccess: (newConfig: AppearanceConfig) => {
18+
queryClient.setQueryData(["appearance"], newConfig);
19+
},
20+
};
21+
};

site/src/api/queries/buildInfo.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as API from "api/api";
2+
import { BuildInfoResponse } from "api/typesGenerated";
3+
import { getMetadataAsJSON } from "utils/metadata";
4+
5+
export const buildInfo = () => {
6+
return {
7+
queryKey: ["buildInfo"],
8+
queryFn: async () =>
9+
getMetadataAsJSON<BuildInfoResponse>("build-info") ?? API.getBuildInfo(),
10+
};
11+
};

site/src/api/queries/entitlements.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { QueryClient } from "@tanstack/react-query";
2+
import * as API from "api/api";
3+
import { Entitlements } from "api/typesGenerated";
4+
import { getMetadataAsJSON } from "utils/metadata";
5+
6+
const ENTITLEMENTS_QUERY_KEY = ["entitlements"];
7+
8+
export const entitlements = () => {
9+
return {
10+
queryKey: ENTITLEMENTS_QUERY_KEY,
11+
queryFn: async () =>
12+
getMetadataAsJSON<Entitlements>("entitlements") ?? API.getEntitlements(),
13+
};
14+
};
15+
16+
export const refreshEntitlements = (queryClient: QueryClient) => {
17+
return {
18+
mutationFn: API.refreshEntitlements,
19+
onSuccess: async () => {
20+
await queryClient.invalidateQueries({
21+
queryKey: ENTITLEMENTS_QUERY_KEY,
22+
});
23+
},
24+
};
25+
};

site/src/api/queries/experiments.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as API from "api/api";
2+
import { Experiments } from "api/typesGenerated";
3+
import { getMetadataAsJSON } from "utils/metadata";
4+
5+
export const experiments = () => {
6+
return {
7+
queryKey: ["experiments"],
8+
queryFn: async () =>
9+
getMetadataAsJSON<Experiments>("experiments") ?? API.getExperiments(),
10+
};
11+
};

site/src/components/Dashboard/DashboardProvider.tsx

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1-
import { useMachine } from "@xstate/react";
1+
import { useQuery } from "@tanstack/react-query";
2+
import { buildInfo } from "api/queries/buildInfo";
3+
import { experiments } from "api/queries/experiments";
4+
import { entitlements } from "api/queries/entitlements";
25
import {
36
AppearanceConfig,
47
BuildInfoResponse,
58
Entitlements,
69
Experiments,
710
} from "api/typesGenerated";
811
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
9-
import { createContext, FC, PropsWithChildren, useContext } from "react";
10-
import { appearanceMachine } from "xServices/appearance/appearanceXService";
11-
import { buildInfoMachine } from "xServices/buildInfo/buildInfoXService";
12-
import { entitlementsMachine } from "xServices/entitlements/entitlementsXService";
13-
import { experimentsMachine } from "xServices/experiments/experimentsMachine";
12+
import {
13+
createContext,
14+
FC,
15+
PropsWithChildren,
16+
useContext,
17+
useState,
18+
} from "react";
19+
import { appearance } from "api/queries/appearance";
1420

1521
interface Appearance {
1622
config: AppearanceConfig;
17-
preview: boolean;
23+
isPreview: boolean;
1824
setPreview: (config: AppearanceConfig) => void;
19-
save: (config: AppearanceConfig) => void;
2025
}
2126

2227
interface DashboardProviderValue {
@@ -31,29 +36,16 @@ export const DashboardProviderContext = createContext<
3136
>(undefined);
3237

3338
export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
34-
const [buildInfoState] = useMachine(buildInfoMachine);
35-
const [entitlementsState] = useMachine(entitlementsMachine);
36-
const [appearanceState, appearanceSend] = useMachine(appearanceMachine);
37-
const [experimentsState] = useMachine(experimentsMachine);
38-
const { buildInfo } = buildInfoState.context;
39-
const { entitlements } = entitlementsState.context;
40-
const { appearance, preview } = appearanceState.context;
41-
const { experiments } = experimentsState.context;
42-
const isLoading = !buildInfo || !entitlements || !appearance || !experiments;
43-
44-
const setAppearancePreview = (config: AppearanceConfig) => {
45-
appearanceSend({
46-
type: "SET_PREVIEW_APPEARANCE",
47-
appearance: config,
48-
});
49-
};
50-
51-
const saveAppearance = (config: AppearanceConfig) => {
52-
appearanceSend({
53-
type: "SAVE_APPEARANCE",
54-
appearance: config,
55-
});
56-
};
39+
const buildInfoQuery = useQuery(buildInfo());
40+
const entitlementsQuery = useQuery(entitlements());
41+
const experimentsQuery = useQuery(experiments());
42+
const appearanceQuery = useQuery(appearance());
43+
const isLoading =
44+
!buildInfoQuery.data ||
45+
!entitlementsQuery.data ||
46+
!appearanceQuery.data ||
47+
!experimentsQuery.data;
48+
const [configPreview, setConfigPreview] = useState<AppearanceConfig>();
5749

5850
if (isLoading) {
5951
return <FullScreenLoader />;
@@ -62,14 +54,13 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
6254
return (
6355
<DashboardProviderContext.Provider
6456
value={{
65-
buildInfo,
66-
entitlements,
67-
experiments,
57+
buildInfo: buildInfoQuery.data,
58+
entitlements: entitlementsQuery.data,
59+
experiments: experimentsQuery.data,
6860
appearance: {
69-
preview,
70-
config: appearance,
71-
setPreview: setAppearancePreview,
72-
save: saveAppearance,
61+
config: configPreview ?? appearanceQuery.data,
62+
setPreview: setConfigPreview,
63+
isPreview: configPreview !== undefined,
7364
},
7465
}}
7566
>

site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const ServiceBanner: React.FC = () => {
1515
<ServiceBannerView
1616
message={message}
1717
backgroundColor={background_color}
18-
preview={appearance.preview}
18+
isPreview={appearance.isPreview}
1919
/>
2020
);
2121
} else {

site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ export const Preview: Story = {
2020
args: {
2121
message: "weeeee",
2222
backgroundColor: "#000000",
23-
preview: true,
23+
isPreview: true,
2424
},
2525
};

site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { hex } from "color-convert";
77
export interface ServiceBannerViewProps {
88
message: string;
99
backgroundColor: string;
10-
preview: boolean;
10+
isPreview: boolean;
1111
}
1212

1313
export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
1414
message,
1515
backgroundColor,
16-
preview,
16+
isPreview,
1717
}) => {
1818
const styles = useStyles();
1919
// We don't want anything funky like an image or a heading in the service
@@ -34,7 +34,7 @@ export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
3434
className={styles.container}
3535
style={{ backgroundColor: backgroundColor }}
3636
>
37-
{preview && <Pill text="Preview" type="info" lightBorder />}
37+
{isPreview && <Pill text="Preview" type="info" lightBorder />}
3838
<div
3939
className={styles.centerContent}
4040
style={{

site/src/components/RequireAuth/RequireAuth.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { embedRedirect } from "../../utils/redirect";
66
import { FullScreenLoader } from "../Loader/FullScreenLoader";
77
import { DashboardProvider } from "components/Dashboard/DashboardProvider";
88
import { ProxyProvider } from "contexts/ProxyContext";
9+
import { isApiError } from "api/errors";
910

1011
export const RequireAuth: FC = () => {
1112
const [authState, authSend] = useAuth();
@@ -18,11 +19,11 @@ export const RequireAuth: FC = () => {
1819
useEffect(() => {
1920
const interceptorHandle = axios.interceptors.response.use(
2021
(okResponse) => okResponse,
21-
(error) => {
22+
(error: unknown) => {
2223
// 401 Unauthorized
2324
// If we encountered an authentication error, then our token is probably
2425
// invalid and we should update the auth state to reflect that.
25-
if (error.response.status === 401) {
26+
if (isApiError(error) && error.response.status === 401) {
2627
authSend("SIGN_OUT");
2728
}
2829

site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@ import {
1212
MockBuildInfo,
1313
MockEntitlementsWithScheduling,
1414
MockExperiments,
15-
MockAppearance,
15+
MockAppearanceConfig,
1616
} from "testHelpers/entities";
1717
import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge";
1818
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider";
1919
import type { Meta, StoryObj } from "@storybook/react";
2020

2121
const MockedAppearance = {
22-
config: MockAppearance,
23-
preview: false,
24-
setPreview: () => null,
25-
save: () => null,
22+
config: MockAppearanceConfig,
23+
isPreview: false,
24+
setPreview: () => {},
2625
};
2726

2827
const meta: Meta<typeof WorkspaceStatusBadge> = {

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/AppearanceSettingsPage/AppearanceSettingsPage.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@ import { FC } from "react";
44
import { Helmet } from "react-helmet-async";
55
import { pageTitle } from "utils/page";
66
import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView";
7+
import { useMutation, useQueryClient } from "@tanstack/react-query";
8+
import { updateAppearance } from "api/queries/appearance";
9+
import { getErrorMessage } from "api/errors";
10+
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
711

812
// ServiceBanner is unlike the other Deployment Settings pages because it
913
// implements a form, whereas the others are read-only. We make this
1014
// exception because the Service Banner is visual, and configuring it from
1115
// the command line would be a significantly worse user experience.
1216
const AppearanceSettingsPage: FC = () => {
1317
const { appearance, entitlements } = useDashboard();
18+
const queryClient = useQueryClient();
19+
const updateAppearanceMutation = useMutation(updateAppearance(queryClient));
1420
const isEntitled =
1521
entitlements.features["appearance"].entitlement !== "not_entitled";
1622

17-
const updateAppearance = (
23+
const onSaveAppearance = async (
1824
newConfig: Partial<UpdateAppearanceConfig>,
1925
preview: boolean,
2026
) => {
@@ -26,7 +32,14 @@ const AppearanceSettingsPage: FC = () => {
2632
appearance.setPreview(newAppearance);
2733
return;
2834
}
29-
appearance.save(newAppearance);
35+
try {
36+
await updateAppearanceMutation.mutateAsync(newAppearance);
37+
displaySuccess("Successfully updated appearance settings!");
38+
} catch (error) {
39+
displayError(
40+
getErrorMessage(error, "Failed to update appearance settings."),
41+
);
42+
}
3043
};
3144

3245
return (
@@ -38,7 +51,7 @@ const AppearanceSettingsPage: FC = () => {
3851
<AppearanceSettingsPageView
3952
appearance={appearance.config}
4053
isEntitled={isEntitled}
41-
updateAppearance={updateAppearance}
54+
onSaveAppearance={onSaveAppearance}
4255
/>
4356
</>
4457
);

site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ const meta: Meta<typeof AppearanceSettingsPageView> = {
1414
},
1515
},
1616
isEntitled: false,
17-
updateAppearance: () => {
18-
return undefined;
19-
},
2017
},
2118
};
2219

2320
export default meta;
2421
type Story = StoryObj<typeof AppearanceSettingsPageView>;
2522

26-
export const Page: Story = {};
23+
export const Entitled: Story = {
24+
args: {
25+
isEntitled: true,
26+
},
27+
};
28+
29+
export const NotEntitled: Story = {};

site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ import { colors } from "theme/colors";
2525
export type AppearanceSettingsPageViewProps = {
2626
appearance: UpdateAppearanceConfig;
2727
isEntitled: boolean;
28-
updateAppearance: (
28+
onSaveAppearance: (
2929
newConfig: Partial<UpdateAppearanceConfig>,
3030
preview: boolean,
3131
) => void;
3232
};
3333
export const AppearanceSettingsPageView = ({
3434
appearance,
3535
isEntitled,
36-
updateAppearance,
36+
onSaveAppearance,
3737
}: AppearanceSettingsPageViewProps): JSX.Element => {
3838
const styles = useStyles();
3939
const theme = useTheme();
@@ -43,7 +43,7 @@ export const AppearanceSettingsPageView = ({
4343
initialValues: {
4444
logo_url: appearance.logo_url,
4545
},
46-
onSubmit: (values) => updateAppearance(values, false),
46+
onSubmit: (values) => onSaveAppearance(values, false),
4747
});
4848
const logoFieldHelpers = getFormHelpers(logoForm);
4949

@@ -56,7 +56,7 @@ export const AppearanceSettingsPageView = ({
5656
appearance.service_banner.background_color ?? colors.blue[7],
5757
},
5858
onSubmit: (values) =>
59-
updateAppearance(
59+
onSaveAppearance(
6060
{
6161
service_banner: values,
6262
},
@@ -123,7 +123,7 @@ export const AppearanceSettingsPageView = ({
123123
!isEntitled && (
124124
<Button
125125
onClick={() => {
126-
updateAppearance(
126+
onSaveAppearance(
127127
{
128128
service_banner: {
129129
message:
@@ -162,7 +162,7 @@ export const AppearanceSettingsPageView = ({
162162
...serviceBannerForm.values,
163163
enabled: newState,
164164
};
165-
updateAppearance(
165+
onSaveAppearance(
166166
{
167167
service_banner: newBanner,
168168
},
@@ -196,7 +196,7 @@ export const AppearanceSettingsPageView = ({
196196
"background_color",
197197
color.hex,
198198
);
199-
updateAppearance(
199+
onSaveAppearance(
200200
{
201201
service_banner: {
202202
...serviceBannerForm.values,

0 commit comments

Comments
 (0)