chore(site): migrate a few services to react-query used in the DashboardProvider (#9667)

This commit is contained in:
Bruno Quaresma
2023-09-14 19:35:03 -03:00
committed by GitHub
parent 3b088a5cb8
commit 79d4179123
29 changed files with 222 additions and 521 deletions
+21
View File
@@ -0,0 +1,21 @@
import { QueryClient } from "@tanstack/react-query";
import * as API from "api/api";
import { AppearanceConfig } from "api/typesGenerated";
import { getMetadataAsJSON } from "utils/metadata";
export const appearance = () => {
return {
queryKey: ["appearance"],
queryFn: async () =>
getMetadataAsJSON<AppearanceConfig>("appearance") ?? API.getAppearance(),
};
};
export const updateAppearance = (queryClient: QueryClient) => {
return {
mutationFn: API.updateAppearance,
onSuccess: (newConfig: AppearanceConfig) => {
queryClient.setQueryData(["appearance"], newConfig);
},
};
};
+11
View File
@@ -0,0 +1,11 @@
import * as API from "api/api";
import { BuildInfoResponse } from "api/typesGenerated";
import { getMetadataAsJSON } from "utils/metadata";
export const buildInfo = () => {
return {
queryKey: ["buildInfo"],
queryFn: async () =>
getMetadataAsJSON<BuildInfoResponse>("build-info") ?? API.getBuildInfo(),
};
};
+25
View File
@@ -0,0 +1,25 @@
import { QueryClient } from "@tanstack/react-query";
import * as API from "api/api";
import { Entitlements } from "api/typesGenerated";
import { getMetadataAsJSON } from "utils/metadata";
const ENTITLEMENTS_QUERY_KEY = ["entitlements"];
export const entitlements = () => {
return {
queryKey: ENTITLEMENTS_QUERY_KEY,
queryFn: async () =>
getMetadataAsJSON<Entitlements>("entitlements") ?? API.getEntitlements(),
};
};
export const refreshEntitlements = (queryClient: QueryClient) => {
return {
mutationFn: API.refreshEntitlements,
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ENTITLEMENTS_QUERY_KEY,
});
},
};
};
+11
View File
@@ -0,0 +1,11 @@
import * as API from "api/api";
import { Experiments } from "api/typesGenerated";
import { getMetadataAsJSON } from "utils/metadata";
export const experiments = () => {
return {
queryKey: ["experiments"],
queryFn: async () =>
getMetadataAsJSON<Experiments>("experiments") ?? API.getExperiments(),
};
};
@@ -1,4 +1,7 @@
import { useMachine } from "@xstate/react";
import { useQuery } from "@tanstack/react-query";
import { buildInfo } from "api/queries/buildInfo";
import { experiments } from "api/queries/experiments";
import { entitlements } from "api/queries/entitlements";
import {
AppearanceConfig,
BuildInfoResponse,
@@ -6,17 +9,19 @@ import {
Experiments,
} from "api/typesGenerated";
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
import { createContext, FC, PropsWithChildren, useContext } from "react";
import { appearanceMachine } from "xServices/appearance/appearanceXService";
import { buildInfoMachine } from "xServices/buildInfo/buildInfoXService";
import { entitlementsMachine } from "xServices/entitlements/entitlementsXService";
import { experimentsMachine } from "xServices/experiments/experimentsMachine";
import {
createContext,
FC,
PropsWithChildren,
useContext,
useState,
} from "react";
import { appearance } from "api/queries/appearance";
interface Appearance {
config: AppearanceConfig;
preview: boolean;
isPreview: boolean;
setPreview: (config: AppearanceConfig) => void;
save: (config: AppearanceConfig) => void;
}
interface DashboardProviderValue {
@@ -31,29 +36,16 @@ export const DashboardProviderContext = createContext<
>(undefined);
export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
const [buildInfoState] = useMachine(buildInfoMachine);
const [entitlementsState] = useMachine(entitlementsMachine);
const [appearanceState, appearanceSend] = useMachine(appearanceMachine);
const [experimentsState] = useMachine(experimentsMachine);
const { buildInfo } = buildInfoState.context;
const { entitlements } = entitlementsState.context;
const { appearance, preview } = appearanceState.context;
const { experiments } = experimentsState.context;
const isLoading = !buildInfo || !entitlements || !appearance || !experiments;
const setAppearancePreview = (config: AppearanceConfig) => {
appearanceSend({
type: "SET_PREVIEW_APPEARANCE",
appearance: config,
});
};
const saveAppearance = (config: AppearanceConfig) => {
appearanceSend({
type: "SAVE_APPEARANCE",
appearance: config,
});
};
const buildInfoQuery = useQuery(buildInfo());
const entitlementsQuery = useQuery(entitlements());
const experimentsQuery = useQuery(experiments());
const appearanceQuery = useQuery(appearance());
const isLoading =
!buildInfoQuery.data ||
!entitlementsQuery.data ||
!appearanceQuery.data ||
!experimentsQuery.data;
const [configPreview, setConfigPreview] = useState<AppearanceConfig>();
if (isLoading) {
return <FullScreenLoader />;
@@ -62,14 +54,13 @@ export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
return (
<DashboardProviderContext.Provider
value={{
buildInfo,
entitlements,
experiments,
buildInfo: buildInfoQuery.data,
entitlements: entitlementsQuery.data,
experiments: experimentsQuery.data,
appearance: {
preview,
config: appearance,
setPreview: setAppearancePreview,
save: saveAppearance,
config: configPreview ?? appearanceQuery.data,
setPreview: setConfigPreview,
isPreview: configPreview !== undefined,
},
}}
>
@@ -15,7 +15,7 @@ export const ServiceBanner: React.FC = () => {
<ServiceBannerView
message={message}
backgroundColor={background_color}
preview={appearance.preview}
isPreview={appearance.isPreview}
/>
);
} else {
@@ -20,6 +20,6 @@ export const Preview: Story = {
args: {
message: "weeeee",
backgroundColor: "#000000",
preview: true,
isPreview: true,
},
};
@@ -7,13 +7,13 @@ import { hex } from "color-convert";
export interface ServiceBannerViewProps {
message: string;
backgroundColor: string;
preview: boolean;
isPreview: boolean;
}
export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
message,
backgroundColor,
preview,
isPreview,
}) => {
const styles = useStyles();
// 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> = ({
className={styles.container}
style={{ backgroundColor: backgroundColor }}
>
{preview && <Pill text="Preview" type="info" lightBorder />}
{isPreview && <Pill text="Preview" type="info" lightBorder />}
<div
className={styles.centerContent}
style={{
@@ -6,6 +6,7 @@ import { embedRedirect } from "../../utils/redirect";
import { FullScreenLoader } from "../Loader/FullScreenLoader";
import { DashboardProvider } from "components/Dashboard/DashboardProvider";
import { ProxyProvider } from "contexts/ProxyContext";
import { isApiError } from "api/errors";
export const RequireAuth: FC = () => {
const [authState, authSend] = useAuth();
@@ -18,11 +19,11 @@ export const RequireAuth: FC = () => {
useEffect(() => {
const interceptorHandle = axios.interceptors.response.use(
(okResponse) => okResponse,
(error) => {
(error: unknown) => {
// 401 Unauthorized
// If we encountered an authentication error, then our token is probably
// invalid and we should update the auth state to reflect that.
if (error.response.status === 401) {
if (isApiError(error) && error.response.status === 401) {
authSend("SIGN_OUT");
}
@@ -12,17 +12,16 @@ import {
MockBuildInfo,
MockEntitlementsWithScheduling,
MockExperiments,
MockAppearance,
MockAppearanceConfig,
} from "testHelpers/entities";
import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge";
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider";
import type { Meta, StoryObj } from "@storybook/react";
const MockedAppearance = {
config: MockAppearance,
preview: false,
setPreview: () => null,
save: () => null,
config: MockAppearanceConfig,
isPreview: false,
setPreview: () => {},
};
const meta: Meta<typeof WorkspaceStatusBadge> = {
+1 -1
View File
@@ -1,6 +1,6 @@
import { FeatureName } from "api/typesGenerated";
import { useDashboard } from "components/Dashboard/DashboardProvider";
import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors";
import { selectFeatureVisibility } from "utils/entitlements";
export const useFeatureVisibility = (): Record<FeatureName, boolean> => {
const { entitlements } = useDashboard();
@@ -4,6 +4,10 @@ import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateAppearance } from "api/queries/appearance";
import { getErrorMessage } from "api/errors";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
// ServiceBanner is unlike the other Deployment Settings pages because it
// implements a form, whereas the others are read-only. We make this
@@ -11,10 +15,12 @@ import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView";
// the command line would be a significantly worse user experience.
const AppearanceSettingsPage: FC = () => {
const { appearance, entitlements } = useDashboard();
const queryClient = useQueryClient();
const updateAppearanceMutation = useMutation(updateAppearance(queryClient));
const isEntitled =
entitlements.features["appearance"].entitlement !== "not_entitled";
const updateAppearance = (
const onSaveAppearance = async (
newConfig: Partial<UpdateAppearanceConfig>,
preview: boolean,
) => {
@@ -26,7 +32,14 @@ const AppearanceSettingsPage: FC = () => {
appearance.setPreview(newAppearance);
return;
}
appearance.save(newAppearance);
try {
await updateAppearanceMutation.mutateAsync(newAppearance);
displaySuccess("Successfully updated appearance settings!");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to update appearance settings."),
);
}
};
return (
@@ -38,7 +51,7 @@ const AppearanceSettingsPage: FC = () => {
<AppearanceSettingsPageView
appearance={appearance.config}
isEntitled={isEntitled}
updateAppearance={updateAppearance}
onSaveAppearance={onSaveAppearance}
/>
</>
);
@@ -14,13 +14,16 @@ const meta: Meta<typeof AppearanceSettingsPageView> = {
},
},
isEntitled: false,
updateAppearance: () => {
return undefined;
},
},
};
export default meta;
type Story = StoryObj<typeof AppearanceSettingsPageView>;
export const Page: Story = {};
export const Entitled: Story = {
args: {
isEntitled: true,
},
};
export const NotEntitled: Story = {};
@@ -25,7 +25,7 @@ import { colors } from "theme/colors";
export type AppearanceSettingsPageViewProps = {
appearance: UpdateAppearanceConfig;
isEntitled: boolean;
updateAppearance: (
onSaveAppearance: (
newConfig: Partial<UpdateAppearanceConfig>,
preview: boolean,
) => void;
@@ -33,7 +33,7 @@ export type AppearanceSettingsPageViewProps = {
export const AppearanceSettingsPageView = ({
appearance,
isEntitled,
updateAppearance,
onSaveAppearance,
}: AppearanceSettingsPageViewProps): JSX.Element => {
const styles = useStyles();
const theme = useTheme();
@@ -43,7 +43,7 @@ export const AppearanceSettingsPageView = ({
initialValues: {
logo_url: appearance.logo_url,
},
onSubmit: (values) => updateAppearance(values, false),
onSubmit: (values) => onSaveAppearance(values, false),
});
const logoFieldHelpers = getFormHelpers(logoForm);
@@ -56,7 +56,7 @@ export const AppearanceSettingsPageView = ({
appearance.service_banner.background_color ?? colors.blue[7],
},
onSubmit: (values) =>
updateAppearance(
onSaveAppearance(
{
service_banner: values,
},
@@ -123,7 +123,7 @@ export const AppearanceSettingsPageView = ({
!isEntitled && (
<Button
onClick={() => {
updateAppearance(
onSaveAppearance(
{
service_banner: {
message:
@@ -162,7 +162,7 @@ export const AppearanceSettingsPageView = ({
...serviceBannerForm.values,
enabled: newState,
};
updateAppearance(
onSaveAppearance(
{
service_banner: newBanner,
},
@@ -196,7 +196,7 @@ export const AppearanceSettingsPageView = ({
"background_color",
color.hex,
);
updateAppearance(
onSaveAppearance(
{
service_banner: {
...serviceBannerForm.values,
@@ -1,5 +1,4 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMachine } from "@xstate/react";
import { getLicenses, removeLicense } from "api/api";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { FC, useEffect } from "react";
@@ -7,22 +6,30 @@ import { Helmet } from "react-helmet-async";
import { useSearchParams } from "react-router-dom";
import useToggle from "react-use/lib/useToggle";
import { pageTitle } from "utils/page";
import { entitlementsMachine } from "xServices/entitlements/entitlementsXService";
import LicensesSettingsPageView from "./LicensesSettingsPageView";
import { getErrorMessage } from "api/errors";
import { entitlements, refreshEntitlements } from "api/queries/entitlements";
const LicensesSettingsPage: FC = () => {
const queryClient = useQueryClient();
const [entitlementsState, sendEvent] = useMachine(entitlementsMachine);
const { entitlements, getEntitlementsError } = entitlementsState.context;
const [searchParams, setSearchParams] = useSearchParams();
const success = searchParams.get("success");
const [confettiOn, toggleConfettiOn] = useToggle(false);
if (getEntitlementsError) {
displayError(
getErrorMessage(getEntitlementsError, "Failed to fetch entitlements"),
);
}
const entitlementsQuery = useQuery(entitlements());
const refreshEntitlementsMutation = useMutation(
refreshEntitlements(queryClient),
);
useEffect(() => {
if (entitlementsQuery.error) {
displayError(
getErrorMessage(
entitlementsQuery.error,
"Failed to fetch entitlements",
),
);
}
}, [entitlementsQuery.error]);
const { mutate: removeLicenseApi, isLoading: isRemovingLicense } =
useMutation(removeLicense, {
@@ -59,14 +66,19 @@ const LicensesSettingsPage: FC = () => {
<LicensesSettingsPageView
showConfetti={confettiOn}
isLoading={isLoading}
userLimitActual={entitlements?.features.user_limit.actual}
userLimitLimit={entitlements?.features.user_limit.limit}
isRefreshing={refreshEntitlementsMutation.isLoading}
userLimitActual={entitlementsQuery.data?.features.user_limit.actual}
userLimitLimit={entitlementsQuery.data?.features.user_limit.limit}
licenses={licenses}
isRemovingLicense={isRemovingLicense}
removeLicense={(licenseId: number) => removeLicenseApi(licenseId)}
refreshEntitlements={() => {
const x = sendEvent("REFRESH");
return !x.context.getEntitlementsError;
refreshEntitlements={async () => {
try {
await refreshEntitlementsMutation.mutateAsync();
displaySuccess("Successfully removed license");
} catch (error) {
displayError(getErrorMessage(error, "Failed to remove license"));
}
}}
/>
</>
@@ -12,8 +12,8 @@ import Confetti from "react-confetti";
import { Link } from "react-router-dom";
import useWindowSize from "react-use/lib/useWindowSize";
import MuiLink from "@mui/material/Link";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import Tooltip from "@mui/material/Tooltip";
import { LoadingButton } from "components/LoadingButton/LoadingButton";
type Props = {
showConfetti: boolean;
@@ -22,8 +22,9 @@ type Props = {
userLimitLimit?: number;
licenses?: GetLicensesResponse[];
isRemovingLicense: boolean;
isRefreshing: boolean;
removeLicense: (licenseId: number) => void;
refreshEntitlements?: () => boolean;
refreshEntitlements: () => void;
};
const LicensesSettingsPageView: FC<Props> = ({
@@ -33,6 +34,7 @@ const LicensesSettingsPageView: FC<Props> = ({
userLimitLimit,
licenses,
isRemovingLicense,
isRefreshing,
removeLicense,
refreshEntitlements,
}) => {
@@ -69,18 +71,13 @@ const LicensesSettingsPageView: FC<Props> = ({
Add a license
</Button>
<Tooltip title="Refresh license entitlements. This is done automatically every 10 minutes.">
<Button
onClick={() => {
if (refreshEntitlements) {
if (refreshEntitlements()) {
displaySuccess("Successfully refreshed licenses");
}
}
}}
<LoadingButton
loading={isRefreshing}
onClick={refreshEntitlements}
startIcon={<RefreshIcon />}
>
Refresh
</Button>
</LoadingButton>
</Tooltip>
</Stack>
</Stack>
@@ -3,8 +3,9 @@ import { ConfirmDialog } from "../../../components/Dialogs/ConfirmDialog/Confirm
import { Section } from "../../../components/SettingsLayout/Section";
import { SSHKeysPageView } from "./SSHKeysPageView";
import { regenerateUserSSHKey, userSSHKey } from "api/queries/sshKeys";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getErrorMessage } from "api/errors";
export const Language = {
title: "SSH keys",
@@ -20,18 +21,9 @@ export const SSHKeysPage: FC<PropsWithChildren<unknown>> = () => {
useState(false);
const queryClient = useQueryClient();
const userSSHKeyQuery = useQuery(userSSHKey("me"));
const regenerateSSHKeyMutationOptions = regenerateUserSSHKey(
"me",
queryClient,
const regenerateSSHKeyMutation = useMutation(
regenerateUserSSHKey("me", queryClient),
);
const regenerateSSHKeyMutation = useMutation({
...regenerateSSHKeyMutationOptions,
onSuccess: (newKey) => {
regenerateSSHKeyMutationOptions.onSuccess(newKey);
displaySuccess("SSH Key regenerated successfully.");
setIsConfirmingRegeneration(false);
},
});
return (
<>
@@ -54,7 +46,18 @@ export const SSHKeysPage: FC<PropsWithChildren<unknown>> = () => {
confirmLoading={regenerateSSHKeyMutation.isLoading}
title={Language.regenerateDialogTitle}
confirmText={Language.confirmLabel}
onConfirm={regenerateSSHKeyMutation.mutate}
onConfirm={async () => {
try {
await regenerateSSHKeyMutation.mutateAsync();
displaySuccess("SSH Key regenerated successfully.");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to regenerate SSH key"),
);
} finally {
setIsConfirmingRegeneration(false);
}
}}
onClose={() => {
setIsConfirmingRegeneration(false);
}}
@@ -11,10 +11,9 @@ import { DashboardProviderContext } from "components/Dashboard/DashboardProvider
import { WorkspaceBuildLogsSection } from "pages/WorkspacePage/WorkspaceBuildLogsSection";
const MockedAppearance = {
config: Mocks.MockAppearance,
preview: false,
setPreview: () => null,
save: () => null,
config: Mocks.MockAppearanceConfig,
isPreview: false,
setPreview: () => {},
};
const meta: Meta<typeof Workspace> = {
@@ -1,7 +1,7 @@
import { Meta, StoryObj } from "@storybook/react";
import {
MockWorkspace,
MockAppearance,
MockAppearanceConfig,
MockBuildInfo,
MockEntitlementsWithScheduling,
MockExperiments,
@@ -10,10 +10,9 @@ import { WorkspaceStats } from "./WorkspaceStats";
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider";
const MockedAppearance = {
config: MockAppearance,
preview: false,
setPreview: () => null,
save: () => null,
config: MockAppearanceConfig,
isPreview: false,
setPreview: () => {},
};
const meta: Meta<typeof WorkspaceStats> = {
@@ -9,7 +9,7 @@ import {
} from "api/typesGenerated";
import {
MockWorkspace,
MockAppearance,
MockAppearanceConfig,
MockBuildInfo,
MockEntitlementsWithScheduling,
MockExperiments,
@@ -71,10 +71,9 @@ const allWorkspaces = [
];
const MockedAppearance = {
config: MockAppearance,
preview: false,
setPreview: () => null,
save: () => null,
config: MockAppearanceConfig,
isPreview: false,
setPreview: () => {},
};
type FilterProps = ComponentProps<typeof WorkspacesPageView>["filterProps"];
+1 -1
View File
@@ -2105,7 +2105,7 @@ export const MockDeploymentConfig: DeploymentConfig = {
options: [],
};
export const MockAppearance: TypesGen.AppearanceConfig = {
export const MockAppearanceConfig: TypesGen.AppearanceConfig = {
logo_url: "",
service_banner: {
enabled: false,
+1 -1
View File
@@ -324,7 +324,7 @@ export const handlers = [
}),
rest.get("/api/v2/appearance", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockAppearance));
return res(ctx.status(200), ctx.json(M.MockAppearanceConfig));
}),
rest.get("/api/v2/deployment/stats", (_, res, ctx) => {
@@ -1,4 +1,4 @@
import { getFeatureVisibility } from "./entitlementsSelectors";
import { getFeatureVisibility } from "./entitlements";
describe("getFeatureVisibility", () => {
it("returns empty object if there is no license", () => {
+18
View File
@@ -0,0 +1,18 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- It can be any
export const getMetadataAsJSON = <T extends Record<string, any>>(
property: string,
): T | undefined => {
const appearance = document.querySelector(`meta[property=${property}]`);
if (appearance) {
const rawContent = appearance.getAttribute("content");
try {
return JSON.parse(rawContent as string);
} catch (ex) {
// In development the metadata is always going to be empty throwing this
// error
if (process.env.NODE_ENV === "production") {
console.warn(`Failed to parse ${property} metadata`);
}
}
}
};
@@ -1,120 +0,0 @@
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { assign, createMachine } from "xstate";
import * as API from "../../api/api";
import { AppearanceConfig } from "../../api/typesGenerated";
import { getErrorMessage } from "api/errors";
export type AppearanceContext = {
appearance?: AppearanceConfig;
getAppearanceError?: unknown;
preview: boolean;
};
export type AppearanceEvent =
| { type: "SET_PREVIEW_APPEARANCE"; appearance: AppearanceConfig }
| { type: "SAVE_APPEARANCE"; appearance: AppearanceConfig };
export const appearanceMachine = createMachine(
{
id: "appearanceMachine",
predictableActionArguments: true,
tsTypes: {} as import("./appearanceXService.typegen").Typegen0,
schema: {
context: {} as AppearanceContext,
events: {} as AppearanceEvent,
services: {
getAppearance: {
data: {} as AppearanceConfig,
},
setAppearance: {
data: {},
},
},
},
context: {
preview: false,
},
initial: "gettingAppearance",
states: {
idle: {
on: {
SET_PREVIEW_APPEARANCE: {
actions: ["clearGetAppearanceError", "assignPreviewAppearance"],
},
SAVE_APPEARANCE: "savingAppearance",
},
},
gettingAppearance: {
entry: "clearGetAppearanceError",
invoke: {
id: "getAppearance",
src: "getAppearance",
onDone: {
target: "idle",
actions: ["assignAppearance"],
},
onError: {
target: "idle",
actions: ["assignGetAppearanceError"],
},
},
},
savingAppearance: {
invoke: {
id: "setAppearance",
src: "setAppearance",
onDone: {
target: "idle",
actions: ["assignAppearance", "notifyUpdateAppearanceSuccess"],
},
onError: {
target: "idle",
actions: (_, error) => {
displayError(
getErrorMessage(error, "Failed to update appearance settings."),
);
},
},
},
},
},
},
{
actions: {
assignPreviewAppearance: assign({
appearance: (_, event) => event.appearance,
preview: (_) => true,
}),
notifyUpdateAppearanceSuccess: () => {
displaySuccess("Successfully updated appearance settings!");
},
assignAppearance: assign({
appearance: (_, event) => event.data as AppearanceConfig,
preview: (_) => false,
}),
assignGetAppearanceError: assign({
getAppearanceError: (_, event) => event.data,
}),
clearGetAppearanceError: assign({
getAppearanceError: (_) => undefined,
}),
},
services: {
getAppearance: async () => {
// Appearance is injected by the Coder server into the HTML document.
const appearance = document.querySelector("meta[property=appearance]");
if (appearance) {
const rawContent = appearance.getAttribute("content");
try {
return JSON.parse(rawContent as string);
} catch (ex) {
// Ignore this and fetch as normal!
}
}
return API.getAppearance();
},
setAppearance: (_, event) => API.updateAppearance(event.appearance),
},
},
);
@@ -1,88 +0,0 @@
import { assign, createMachine } from "xstate";
import * as API from "../../api/api";
import * as TypesGen from "../../api/typesGenerated";
export interface BuildInfoContext {
getBuildInfoError?: unknown;
buildInfo?: TypesGen.BuildInfoResponse;
}
export const buildInfoMachine = createMachine(
{
id: "buildInfoState",
predictableActionArguments: true,
tsTypes: {} as import("./buildInfoXService.typegen").Typegen0,
schema: {
context: {} as BuildInfoContext,
services: {} as {
getBuildInfo: {
data: TypesGen.BuildInfoResponse;
};
},
},
context: {
buildInfo: undefined,
},
initial: "gettingBuildInfo",
states: {
gettingBuildInfo: {
invoke: {
src: "getBuildInfo",
id: "getBuildInfo",
onDone: [
{
actions: ["assignBuildInfo", "clearGetBuildInfoError"],
target: "#buildInfoState.success",
},
],
onError: [
{
actions: ["assignGetBuildInfoError", "clearBuildInfo"],
target: "#buildInfoState.failure",
},
],
},
},
success: {
type: "final",
},
failure: {
type: "final",
},
},
},
{
services: {
getBuildInfo: async () => {
// Build info is injected by the Coder server into the HTML document.
const buildInfo = document.querySelector("meta[property=build-info]");
if (buildInfo) {
const rawContent = buildInfo.getAttribute("content");
try {
return JSON.parse(rawContent as string);
} catch (ex) {
// Ignore this and fetch as normal!
}
}
return API.getBuildInfo();
},
},
actions: {
assignBuildInfo: assign({
buildInfo: (_, event) => event.data,
}),
clearBuildInfo: assign((context: BuildInfoContext) => ({
...context,
buildInfo: undefined,
})),
assignGetBuildInfoError: assign({
getBuildInfoError: (_, event) => event.data,
}),
clearGetBuildInfoError: assign((context: BuildInfoContext) => ({
...context,
getBuildInfoError: undefined,
})),
},
},
);
@@ -1,106 +0,0 @@
import { assign, createMachine } from "xstate";
import * as API from "../../api/api";
import { Entitlements } from "../../api/typesGenerated";
export type EntitlementsContext = {
entitlements?: Entitlements;
getEntitlementsError?: unknown;
};
export const entitlementsMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5RgHYBcCWaA2YC2qasAsgIYDGAFhimAHQBOYAZk7JQMQQD2tdNAN24Brek1ZxKAUXRZcBdLADaABgC6iUAAdusLBl6aQAD0QAWAGwAOOgGYAjACYArABoQAT0SOA7AE46C0czP2cVHzNIlRV7HwBfOPdCOXxCEgpqPnE2TjAGBm4GOi1sUjRmQrxGFhyZTBxUxVUNJBAdPUxDVtMESxsHF3cvBFtbAKcQsIiomPjE8FkGhSIyKhp6GDRMFCg6lOXYLl56QRENsDQ9pbTmo3b9LtAe2wtnOmcBi1Hos0c-Hx8Q0QVnsdBCfj8Vh8UKsYVscySi3kaVWmXOWxouyRjSIHDyBSKJTKFQYVU2V2RTXUd10DxQRmer3en2+Kl+-0BnkQsTM7yCZimkTM0ViCUR9UpKwy634EFwHAASlIAGJKgDKAAlbq17p16d1uZCgb1HBZ3n54fZIrCgrZHGKFhKcek1nx8YVFSr1VrqTraXqjMN7KE6CoLT4rWYbY4HJyev5QabnBYzA4LX5XmYEvMUNwIHAjMlropUesaR0DPqnogALRR401s3RaKORwqWyw2Ehe3zIuSl1o6oSdjlukMxDwlR0HwWAFsiz-PwqZz-Y0r3m2AXhaGhPxmHzOB1952lvibbZYp0HUcBg0m42wuwp5wfeHP9s98X7FHSvgYOVgDelbjggKjGiENgrpa1rJjGn6Ot+Ja-vQsAAK7kOQcDwH6FaPCYiBQXQSYpmmYyZsa9gqI4oYDM49gWMGPimrOR7Ygcp70O6DBAXhPSEcRqbBmRzhmBRIZhtBUawbG2ZxEAA */
id: "entitlementsMachine",
predictableActionArguments: true,
tsTypes: {} as import("./entitlementsXService.typegen").Typegen0,
schema: {
context: {} as EntitlementsContext,
services: {
getEntitlements: {
data: {} as Entitlements,
},
},
},
initial: "gettingEntitlements",
states: {
refresh: {
invoke: {
id: "refreshEntitlements",
src: "refreshEntitlements",
onDone: {
target: "gettingEntitlements",
},
onError: {
target: "error",
actions: ["assignGetEntitlementsError"],
},
},
entry: "clearGetEntitlementsError",
},
gettingEntitlements: {
entry: "clearGetEntitlementsError",
invoke: {
id: "getEntitlements",
src: "getEntitlements",
onDone: {
target: "idle",
actions: ["assignEntitlements"],
},
onError: {
target: "error",
actions: ["assignGetEntitlementsError"],
},
},
},
idle: {
on: {
REFRESH: "refresh",
},
},
success: {
type: "final",
},
error: {
on: {
REFRESH: "refresh",
},
},
},
},
{
actions: {
assignEntitlements: assign({
entitlements: (_, event) => event.data,
}),
assignGetEntitlementsError: assign({
getEntitlementsError: (_, event) => {
return event.data;
},
}),
clearGetEntitlementsError: assign({
getEntitlementsError: (_) => undefined,
}),
},
services: {
refreshEntitlements: async () => {
return API.refreshEntitlements();
},
getEntitlements: async () => {
// Entitlements is injected by the Coder server into the HTML document.
const entitlements = document.querySelector(
"meta[property=entitlements]",
);
if (entitlements) {
const rawContent = entitlements.getAttribute("content");
try {
return JSON.parse(rawContent as string);
} catch (ex) {
// Ignore this and fetch as normal!
}
}
return API.getEntitlements();
},
},
},
);
@@ -1,87 +0,0 @@
import { getExperiments } from "api/api";
import { Experiment } from "api/typesGenerated";
import { createMachine, assign } from "xstate";
export interface ExperimentsContext {
experiments?: Experiment[];
getExperimentsError?: unknown;
}
export const experimentsMachine = createMachine(
{
id: "experimentsState",
predictableActionArguments: true,
tsTypes: {} as import("./experimentsMachine.typegen").Typegen0,
schema: {
context: {} as ExperimentsContext,
services: {} as {
getExperiments: {
data: Experiment[];
};
},
},
initial: "gettingExperiments",
states: {
gettingExperiments: {
invoke: {
src: "getExperiments",
id: "getExperiments",
onDone: [
{
actions: ["assignExperiments", "clearGetExperimentsError"],
target: "#experimentsState.success",
},
],
onError: [
{
actions: ["assignGetExperimentsError", "clearExperiments"],
target: "#experimentsState.failure",
},
],
},
},
success: {
type: "final",
},
failure: {
type: "final",
},
},
},
{
services: {
getExperiments: async () => {
// Experiments is injected by the Coder server into the HTML document.
const experiments = document.querySelector(
"meta[property=experiments]",
);
if (experiments) {
const rawContent = experiments.getAttribute("content");
try {
return JSON.parse(rawContent as string);
} catch (ex) {
// Ignore this and fetch as normal!
}
}
return getExperiments();
},
},
actions: {
assignExperiments: assign({
experiments: (_, event) => event.data,
}),
clearExperiments: assign((context: ExperimentsContext) => ({
...context,
experiments: undefined,
})),
assignGetExperimentsError: assign({
getExperimentsError: (_, event) => event.data,
}),
clearGetExperimentsError: assign((context: ExperimentsContext) => ({
...context,
getExperimentsError: undefined,
})),
},
},
);