diff --git a/site/src/api/queries/appearance.ts b/site/src/api/queries/appearance.ts new file mode 100644 index 0000000000000..469875d6ce8d6 --- /dev/null +++ b/site/src/api/queries/appearance.ts @@ -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("appearance") ?? API.getAppearance(), + }; +}; + +export const updateAppearance = (queryClient: QueryClient) => { + return { + mutationFn: API.updateAppearance, + onSuccess: (newConfig: AppearanceConfig) => { + queryClient.setQueryData(["appearance"], newConfig); + }, + }; +}; diff --git a/site/src/api/queries/buildInfo.ts b/site/src/api/queries/buildInfo.ts new file mode 100644 index 0000000000000..af90ffacef0be --- /dev/null +++ b/site/src/api/queries/buildInfo.ts @@ -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("build-info") ?? API.getBuildInfo(), + }; +}; diff --git a/site/src/api/queries/entitlements.ts b/site/src/api/queries/entitlements.ts new file mode 100644 index 0000000000000..7284c957b06bc --- /dev/null +++ b/site/src/api/queries/entitlements.ts @@ -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") ?? API.getEntitlements(), + }; +}; + +export const refreshEntitlements = (queryClient: QueryClient) => { + return { + mutationFn: API.refreshEntitlements, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ENTITLEMENTS_QUERY_KEY, + }); + }, + }; +}; diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts new file mode 100644 index 0000000000000..cc6a2a067fa1d --- /dev/null +++ b/site/src/api/queries/experiments.ts @@ -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") ?? API.getExperiments(), + }; +}; diff --git a/site/src/components/Dashboard/DashboardProvider.tsx b/site/src/components/Dashboard/DashboardProvider.tsx index bfe632d755d9e..e8f6485777937 100644 --- a/site/src/components/Dashboard/DashboardProvider.tsx +++ b/site/src/components/Dashboard/DashboardProvider.tsx @@ -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 = ({ 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(); if (isLoading) { return ; @@ -62,14 +54,13 @@ export const DashboardProvider: FC = ({ children }) => { return ( diff --git a/site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx b/site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx index 3f5a65f0d717b..239e6905ef523 100644 --- a/site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx +++ b/site/src/components/Dashboard/ServiceBanner/ServiceBanner.tsx @@ -15,7 +15,7 @@ export const ServiceBanner: React.FC = () => { ); } else { diff --git a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx index 154e7daea5949..8f34bce0eec6a 100644 --- a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx +++ b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.stories.tsx @@ -20,6 +20,6 @@ export const Preview: Story = { args: { message: "weeeee", backgroundColor: "#000000", - preview: true, + isPreview: true, }, }; diff --git a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx index 71d99eac171d6..7432df73d72be 100644 --- a/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx +++ b/site/src/components/Dashboard/ServiceBanner/ServiceBannerView.tsx @@ -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 = ({ 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 = ({ className={styles.container} style={{ backgroundColor: backgroundColor }} > - {preview && } + {isPreview && }
{ 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"); } diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx index 5bb832adc6346..691532de06849 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx @@ -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 = { diff --git a/site/src/hooks/useFeatureVisibility.ts b/site/src/hooks/useFeatureVisibility.ts index 628fad562f20c..00e491a6e303d 100644 --- a/site/src/hooks/useFeatureVisibility.ts +++ b/site/src/hooks/useFeatureVisibility.ts @@ -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 => { const { entitlements } = useDashboard(); diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx index 830fef0fb8684..64ecdf4f887ca 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx @@ -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, 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 = () => { ); diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx index da41824db556c..f628aa82c9fa6 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.stories.tsx @@ -14,13 +14,16 @@ const meta: Meta = { }, }, isEntitled: false, - updateAppearance: () => { - return undefined; - }, }, }; export default meta; type Story = StoryObj; -export const Page: Story = {}; +export const Entitled: Story = { + args: { + isEntitled: true, + }, +}; + +export const NotEntitled: Story = {}; diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx index 313fa5b371be8..363672e63efeb 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx @@ -25,7 +25,7 @@ import { colors } from "theme/colors"; export type AppearanceSettingsPageViewProps = { appearance: UpdateAppearanceConfig; isEntitled: boolean; - updateAppearance: ( + onSaveAppearance: ( newConfig: Partial, 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 && ( - + diff --git a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx index b35cb2688707f..5f889b4a51f50 100644 --- a/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx +++ b/site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx @@ -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> = () => { 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> = () => { 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); }} diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index d06e70f331f7f..dddbf55a2d2b5 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -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 = { diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx index 715bbad70e2f9..43f1f1fec5f1a 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx @@ -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 = { diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 7b308cec65d63..b7d8bcd24c6c7 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -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["filterProps"]; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 9e9f3c3a067f1..ace4333128026 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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, diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 57b87393ee164..7b3cde88d0ca4 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -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) => { diff --git a/site/src/xServices/entitlements/entitlementsSelectors.test.ts b/site/src/utils/entitlements.test.ts similarity index 95% rename from site/src/xServices/entitlements/entitlementsSelectors.test.ts rename to site/src/utils/entitlements.test.ts index f1c3dc56cb46b..b2f473e4a6d9a 100644 --- a/site/src/xServices/entitlements/entitlementsSelectors.test.ts +++ b/site/src/utils/entitlements.test.ts @@ -1,4 +1,4 @@ -import { getFeatureVisibility } from "./entitlementsSelectors"; +import { getFeatureVisibility } from "./entitlements"; describe("getFeatureVisibility", () => { it("returns empty object if there is no license", () => { diff --git a/site/src/xServices/entitlements/entitlementsSelectors.ts b/site/src/utils/entitlements.ts similarity index 100% rename from site/src/xServices/entitlements/entitlementsSelectors.ts rename to site/src/utils/entitlements.ts diff --git a/site/src/utils/metadata.ts b/site/src/utils/metadata.ts new file mode 100644 index 0000000000000..56c2a8323839f --- /dev/null +++ b/site/src/utils/metadata.ts @@ -0,0 +1,18 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- It can be any +export const getMetadataAsJSON = >( + 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`); + } + } + } +}; diff --git a/site/src/xServices/appearance/appearanceXService.ts b/site/src/xServices/appearance/appearanceXService.ts deleted file mode 100644 index cd6f0bc949c1e..0000000000000 --- a/site/src/xServices/appearance/appearanceXService.ts +++ /dev/null @@ -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), - }, - }, -); diff --git a/site/src/xServices/buildInfo/buildInfoXService.ts b/site/src/xServices/buildInfo/buildInfoXService.ts deleted file mode 100644 index 58f2905d5713f..0000000000000 --- a/site/src/xServices/buildInfo/buildInfoXService.ts +++ /dev/null @@ -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, - })), - }, - }, -); diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts deleted file mode 100644 index 61420ace5fc72..0000000000000 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ /dev/null @@ -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(); - }, - }, - }, -); diff --git a/site/src/xServices/experiments/experimentsMachine.ts b/site/src/xServices/experiments/experimentsMachine.ts deleted file mode 100644 index d96a59559cf14..0000000000000 --- a/site/src/xServices/experiments/experimentsMachine.ts +++ /dev/null @@ -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, - })), - }, - }, -);