diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx
index 94ecbd591343e..715a4b080bb96 100644
--- a/site/src/AppRouter.tsx
+++ b/site/src/AppRouter.tsx
@@ -1,6 +1,4 @@
-import { useSelector } from "@xstate/react"
import { FullScreenLoader } from "components/Loader/FullScreenLoader"
-import { RequirePermission } from "components/RequirePermission/RequirePermission"
import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"
import { UsersLayout } from "components/UsersLayout/UsersLayout"
import IndexPage from "pages"
@@ -12,12 +10,9 @@ import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSetting
import TemplatesPage from "pages/TemplatesPage/TemplatesPage"
import UsersPage from "pages/UsersPage/UsersPage"
import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage"
-import { FC, lazy, Suspense, useContext } from "react"
-import { Route, Routes } from "react-router-dom"
-import { selectPermissions } from "xServices/auth/authSelectors"
-import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors"
-import { XServiceContext } from "xServices/StateContext"
-import { DashboardLayout } from "./components/DashboardLayout/DashboardLayout"
+import { FC, lazy, Suspense } from "react"
+import { Route, Routes, BrowserRouter as Router } from "react-router-dom"
+import { DashboardLayout } from "./components/Dashboard/DashboardLayout"
import { RequireAuth } from "./components/RequireAuth/RequireAuth"
import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout"
import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout"
@@ -123,135 +118,119 @@ const CreateTemplatePage = lazy(
)
export const AppRouter: FC = () => {
- const xServices = useContext(XServiceContext)
- const permissions = useSelector(xServices.authXService, selectPermissions)
- const featureVisibility = useSelector(
- xServices.entitlementsXService,
- selectFeatureVisibility,
- )
-
return (
}>
-
- } />
- } />
+
+
+ } />
+ } />
- {/* Dashboard routes */}
- }>
- }>
- } />
+ {/* Dashboard routes */}
+ }>
+ }>
+ } />
- } />
+ } />
- } />
+ } />
-
- } />
- } />
-
+
+ } />
+ } />
+
-
- } />
- } />
-
- }>
- } />
- }
- />
+
+ } />
+ } />
+
+ }>
+ } />
+ }
+ />
+
+
+ } />
+ } />
+
+ } />
+
+
- } />
- } />
-
- } />
+
+ }>
+ } />
-
-
-
- }>
- } />
+ } />
- } />
-
+
+ }>
+ } />
+
-
- }>
- } />
+ } />
+ } />
+ }
+ />
- } />
- } />
- } />
-
+ } />
-
-
-
- }
- />
-
-
- }
- >
- } />
- } />
- } />
- } />
- } />
- } />
-
+ path="/settings/deployment"
+ element={}
+ >
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
- }>
- } />
- } />
- } />
- } />
-
+ }>
+ } />
+ } />
+ } />
+ } />
+
-
-
- } />
- } />
- }
- />
- }
- />
+
+
+ } />
+ } />
+ }
+ />
+ }
+ />
+
-
- {/* Terminal and CLI auth pages don't have the dashboard layout */}
- }
- />
- } />
-
+ {/* Terminal and CLI auth pages don't have the dashboard layout */}
+ }
+ />
+ } />
+
- {/* Using path="*"" means "match anything", so this route
+ {/* Using path="*"" means "match anything", so this route
acts like a catch-all for URLs that we don't have explicit
routes for. */}
- } />
-
+ } />
+
+
)
}
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index f1798e2886f33..a02683bece396 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -34,18 +34,6 @@ export const withDefaultFeatures = (
return fs as TypesGen.Entitlements["features"]
}
-// defaultEntitlements has a default set of disabled functionality.
-export const defaultEntitlements = (): TypesGen.Entitlements => {
- return {
- features: withDefaultFeatures({}),
- has_license: false,
- errors: [],
- warnings: [],
- experimental: false,
- trial: false,
- }
-}
-
// Always attach CSRF token to all requests.
// In puppeteer the document is undefined. In those cases, just
// do nothing.
@@ -625,15 +613,8 @@ export const putWorkspaceExtension = async (
}
export const getEntitlements = async (): Promise => {
- try {
- const response = await axios.get("/api/v2/entitlements")
- return response.data
- } catch (error) {
- if (axios.isAxiosError(error) && error.response?.status === 404) {
- return defaultEntitlements()
- }
- throw error
- }
+ const response = await axios.get("/api/v2/entitlements")
+ return response.data
}
export const getExperiments = async (): Promise => {
diff --git a/site/src/app.tsx b/site/src/app.tsx
index 9c7ece503b878..5dbc2d5e7fcd5 100644
--- a/site/src/app.tsx
+++ b/site/src/app.tsx
@@ -1,29 +1,26 @@
import CssBaseline from "@material-ui/core/CssBaseline"
import ThemeProvider from "@material-ui/styles/ThemeProvider"
+import { AuthProvider } from "components/AuthProvider/AuthProvider"
import { FC } from "react"
import { HelmetProvider } from "react-helmet-async"
-import { BrowserRouter as Router } from "react-router-dom"
import { AppRouter } from "./AppRouter"
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary"
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar"
import { dark } from "./theme"
import "./theme/globalFonts"
-import { XServiceProvider } from "./xServices/StateContext"
export const App: FC = () => {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/site/src/components/AuthProvider/AuthProvider.tsx b/site/src/components/AuthProvider/AuthProvider.tsx
new file mode 100644
index 0000000000000..1b113e80fc155
--- /dev/null
+++ b/site/src/components/AuthProvider/AuthProvider.tsx
@@ -0,0 +1,36 @@
+import { useActor, useInterpret } from "@xstate/react"
+import { createContext, FC, PropsWithChildren, useContext } from "react"
+import { authMachine } from "xServices/auth/authXService"
+import { ActorRefFrom } from "xstate"
+
+interface AuthContextValue {
+ authService: ActorRefFrom
+}
+
+const AuthContext = createContext(undefined)
+
+export const AuthProvider: FC = ({ children }) => {
+ const authService = useInterpret(authMachine)
+
+ return (
+
+ {children}
+
+ )
+}
+
+type UseAuthReturnType = ReturnType<
+ typeof useActor
+>
+
+export const useAuth = (): UseAuthReturnType => {
+ const context = useContext(AuthContext)
+
+ if (!context) {
+ throw new Error("useAuth should be used inside of ")
+ }
+
+ const auth = useActor(context.authService)
+
+ return auth
+}
diff --git a/site/src/components/DashboardLayout/DashboardLayout.tsx b/site/src/components/Dashboard/DashboardLayout.tsx
similarity index 95%
rename from site/src/components/DashboardLayout/DashboardLayout.tsx
rename to site/src/components/Dashboard/DashboardLayout.tsx
index bd20eaaed0c36..920c3cfe30c31 100644
--- a/site/src/components/DashboardLayout/DashboardLayout.tsx
+++ b/site/src/components/Dashboard/DashboardLayout.tsx
@@ -11,6 +11,7 @@ import { ServiceBanner } from "components/ServiceBanner/ServiceBanner"
import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService"
import { usePermissions } from "hooks/usePermissions"
import { UpdateCheckResponse } from "api/typesGenerated"
+import { DashboardProvider } from "./DashboardProvider"
export const DashboardLayout: FC = () => {
const styles = useStyles()
@@ -23,7 +24,7 @@ export const DashboardLayout: FC = () => {
const { error: updateCheckError, updateCheck } = updateCheckState.context
return (
- <>
+
@@ -50,7 +51,7 @@ export const DashboardLayout: FC = () => {
- >
+
)
}
diff --git a/site/src/components/Dashboard/DashboardProvider.tsx b/site/src/components/Dashboard/DashboardProvider.tsx
new file mode 100644
index 0000000000000..fd9065abdb27a
--- /dev/null
+++ b/site/src/components/Dashboard/DashboardProvider.tsx
@@ -0,0 +1,89 @@
+import { useMachine } from "@xstate/react"
+import {
+ AppearanceConfig,
+ BuildInfoResponse,
+ Entitlements,
+ 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"
+
+interface Appearance {
+ config: AppearanceConfig
+ preview: boolean
+ setPreview: (config: AppearanceConfig) => void
+ save: (config: AppearanceConfig) => void
+}
+
+interface DashboardProviderValue {
+ buildInfo: BuildInfoResponse
+ entitlements: Entitlements
+ appearance: Appearance
+ experiments: Experiments
+}
+
+export const DashboardProviderContext = createContext<
+ DashboardProviderValue | undefined
+>(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,
+ })
+ }
+
+ if (isLoading) {
+ return
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useDashboard = (): DashboardProviderValue => {
+ const context = useContext(DashboardProviderContext)
+
+ if (!context) {
+ throw new Error("useDashboard only can be used inside of DashboardProvider")
+ }
+
+ return context
+}
diff --git a/site/src/components/LicenseBanner/LicenseBanner.test.tsx b/site/src/components/LicenseBanner/LicenseBanner.test.tsx
deleted file mode 100644
index f45ac75d0ffa6..0000000000000
--- a/site/src/components/LicenseBanner/LicenseBanner.test.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { screen } from "@testing-library/react"
-import { rest } from "msw"
-import { MockEntitlementsWithWarnings } from "testHelpers/entities"
-import { render } from "testHelpers/renderHelpers"
-import { server } from "testHelpers/server"
-import { LicenseBanner } from "./LicenseBanner"
-import { Language } from "./LicenseBannerView"
-
-describe("LicenseBanner", () => {
- it("does not show when there are no warnings", async () => {
- render()
- const bannerPillSingular = await screen.queryByText(Language.licenseIssue)
- const bannerPillPlural = await screen.queryByText(Language.licenseIssues(2))
- expect(bannerPillSingular).toBe(null)
- expect(bannerPillPlural).toBe(null)
- })
- it("shows when there are warnings", async () => {
- server.use(
- rest.get("/api/v2/entitlements", (req, res, ctx) => {
- return res(ctx.status(200), ctx.json(MockEntitlementsWithWarnings))
- }),
- )
- render()
- const bannerPill = await screen.findByText(Language.licenseIssues(2))
- expect(bannerPill).toBeDefined()
- })
-})
diff --git a/site/src/components/LicenseBanner/LicenseBanner.tsx b/site/src/components/LicenseBanner/LicenseBanner.tsx
index 7ecfc2a2a2fac..8de586ff9e1aa 100644
--- a/site/src/components/LicenseBanner/LicenseBanner.tsx
+++ b/site/src/components/LicenseBanner/LicenseBanner.tsx
@@ -1,19 +1,9 @@
-import { useActor } from "@xstate/react"
-import { useContext, useEffect } from "react"
-import { XServiceContext } from "xServices/StateContext"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
import { LicenseBannerView } from "./LicenseBannerView"
export const LicenseBanner: React.FC = () => {
- const xServices = useContext(XServiceContext)
- const [entitlementsState, entitlementsSend] = useActor(
- xServices.entitlementsXService,
- )
- const { errors, warnings } = entitlementsState.context.entitlements
-
- /** Gets license data on app mount because LicenseBanner is mounted in App */
- useEffect(() => {
- entitlementsSend("GET_ENTITLEMENTS")
- }, [entitlementsSend])
+ const { entitlements } = useDashboard()
+ const { errors, warnings } = entitlements
if (errors.length > 0 || warnings.length > 0) {
return
diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx
index 7941a95b1b92a..c7090178f2d5d 100644
--- a/site/src/components/Navbar/Navbar.tsx
+++ b/site/src/components/Navbar/Navbar.tsx
@@ -1,30 +1,27 @@
-import { shallowEqual, useActor, useSelector } from "@xstate/react"
-import { useContext, FC } from "react"
-import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors"
-import { XServiceContext } from "../../xServices/StateContext"
+import { useAuth } from "components/AuthProvider/AuthProvider"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
+import { useFeatureVisibility } from "hooks/useFeatureVisibility"
+import { useMe } from "hooks/useMe"
+import { usePermissions } from "hooks/usePermissions"
+import { FC } from "react"
import { NavbarView } from "../NavbarView/NavbarView"
export const Navbar: FC = () => {
- const xServices = useContext(XServiceContext)
- const [appearanceState] = useActor(xServices.appearanceXService)
- const [authState, authSend] = useActor(xServices.authXService)
- const [buildInfoState] = useActor(xServices.buildInfoXService)
- const { me, permissions } = authState.context
- const featureVisibility = useSelector(
- xServices.entitlementsXService,
- selectFeatureVisibility,
- shallowEqual,
- )
+ const { appearance, buildInfo } = useDashboard()
+ const [_, authSend] = useAuth()
+ const me = useMe()
+ const permissions = usePermissions()
+ const featureVisibility = useFeatureVisibility()
const canViewAuditLog =
- featureVisibility["audit_log"] && Boolean(permissions?.viewAuditLog)
- const canViewDeployment = Boolean(permissions?.viewDeploymentConfig)
+ featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog)
+ const canViewDeployment = Boolean(permissions.viewDeploymentConfig)
const onSignOut = () => authSend("SIGN_OUT")
return (
{
- const xServices = useContext(XServiceContext)
- const [authState] = useActor(xServices.authXService)
+ const [authState] = useAuth()
const location = useLocation()
const isHomePage = location.pathname === "/"
const navigateTo = isHomePage ? "/login" : embedRedirect(location.pathname)
diff --git a/site/src/components/ServiceBanner/ServiceBanner.tsx b/site/src/components/ServiceBanner/ServiceBanner.tsx
index a8d7068d1693b..d76e7053ea9ef 100644
--- a/site/src/components/ServiceBanner/ServiceBanner.tsx
+++ b/site/src/components/ServiceBanner/ServiceBanner.tsx
@@ -1,22 +1,10 @@
-import { useActor } from "@xstate/react"
-import { useContext, useEffect } from "react"
-import { XServiceContext } from "xServices/StateContext"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
import { ServiceBannerView } from "./ServiceBannerView"
export const ServiceBanner: React.FC = () => {
- const xServices = useContext(XServiceContext)
- const [appearanceState, appearanceSend] = useActor(
- xServices.appearanceXService,
- )
- const [authState] = useActor(xServices.authXService)
+ const { appearance } = useDashboard()
const { message, background_color, enabled } =
- appearanceState.context.appearance.service_banner
-
- useEffect(() => {
- if (authState.matches("signedIn")) {
- appearanceSend("GET_APPEARANCE")
- }
- }, [appearanceSend, authState])
+ appearance.config.service_banner
if (!enabled) {
return null
@@ -27,7 +15,7 @@ export const ServiceBanner: React.FC = () => {
)
} else {
diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.test.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.test.tsx
index b4bd6aa15c663..df5268883ad2e 100644
--- a/site/src/components/SettingsAccountForm/SettingsAccountForm.test.tsx
+++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.test.tsx
@@ -5,7 +5,7 @@ import { AccountForm, AccountFormValues } from "./SettingsAccountForm"
// NOTE: it does not matter what the role props of MockUser are set to,
// only that editable is set to true or false. This is passed from
-// the call to /authorization done by authXService
+// the call to /authorization done by auth provider
describe("AccountForm", () => {
describe("when editable is set to true", () => {
it("allows updating username", async () => {
diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx
index 2a34c7d8ccefe..29e49f54338c4 100644
--- a/site/src/components/TemplateLayout/TemplateLayout.tsx
+++ b/site/src/components/TemplateLayout/TemplateLayout.tsx
@@ -4,7 +4,7 @@ import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
-import { useMachine, useSelector } from "@xstate/react"
+import { useMachine } from "@xstate/react"
import {
PageHeader,
PageHeaderSubtitle,
@@ -20,8 +20,6 @@ import {
} from "react-router-dom"
import { combineClasses } from "util/combineClasses"
import { firstLetter } from "util/firstLetter"
-import { selectPermissions } from "xServices/auth/authSelectors"
-import { XServiceContext } from "xServices/StateContext"
import {
TemplateContext,
templateMachine,
@@ -30,6 +28,7 @@ import { Margins } from "components/Margins/Margins"
import { Stack } from "components/Stack/Stack"
import { Permissions } from "xServices/auth/authXService"
import { Loader } from "components/Loader/Loader"
+import { usePermissions } from "hooks/usePermissions"
const Language = {
settingsButton: "Settings",
@@ -108,8 +107,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
},
})
const { template, permissions: templatePermissions } = templateState.context
- const xServices = useContext(XServiceContext)
- const permissions = useSelector(xServices.authXService, selectPermissions)
+ const permissions = usePermissions()
const hasIcon = template && template.icon && template.icon !== ""
if (!template) {
diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx
index e2845052c695d..dff7b7ce6f567 100644
--- a/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx
+++ b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx
@@ -1,5 +1,6 @@
import { Story } from "@storybook/react"
import * as Mocks from "../../testHelpers/renderHelpers"
+import { MockWorkspace } from "testHelpers/renderHelpers"
import {
WorkspaceStats,
WorkspaceStatsProps,
@@ -18,3 +19,11 @@ export const Example = Template.bind({})
Example.args = {
workspace: Mocks.MockWorkspace,
}
+
+export const Outdated = Template.bind({})
+Outdated.args = {
+ workspace: {
+ ...MockWorkspace,
+ outdated: true,
+ },
+}
diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.test.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.test.tsx
deleted file mode 100644
index 829195e1dbe9e..0000000000000
--- a/site/src/components/WorkspaceStats/WorkspaceStats.test.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { fireEvent, screen } from "@testing-library/react"
-import { Language } from "components/Tooltips/OutdatedHelpTooltip"
-import { WorkspaceStats } from "components/WorkspaceStats/WorkspaceStats"
-import { MockOutdatedWorkspace } from "testHelpers/entities"
-import { renderWithAuth } from "testHelpers/renderHelpers"
-import * as CreateDayString from "util/createDayString"
-
-describe("WorkspaceStats", () => {
- it("shows an outdated tooltip", async () => {
- // Mocking the dayjs module within the createDayString file
- const mock = jest.spyOn(CreateDayString, "createDayString")
- mock.mockImplementation(() => "a minute ago")
-
- const handleUpdateMock = jest.fn()
- renderWithAuth(
- ,
- {
- route: `/@${MockOutdatedWorkspace.owner_name}/${MockOutdatedWorkspace.name}`,
- path: "/@:username/:workspace",
- },
- )
- const tooltipButton = await screen.findByRole("button")
- fireEvent.click(tooltipButton)
- expect(
- await screen.findByText(Language.versionTooltipText),
- ).toBeInTheDocument()
- const updateButton = screen.getByRole("button", {
- name: "update version",
- })
- fireEvent.click(updateButton)
- expect(handleUpdateMock).toBeCalledTimes(1)
- })
-})
diff --git a/site/src/hooks/useFeatureVisibility.ts b/site/src/hooks/useFeatureVisibility.ts
index 7e0a860972c58..d12c7016854eb 100644
--- a/site/src/hooks/useFeatureVisibility.ts
+++ b/site/src/hooks/useFeatureVisibility.ts
@@ -1,10 +1,8 @@
-import { useSelector } from "@xstate/react"
import { FeatureName } from "api/typesGenerated"
-import { useContext } from "react"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors"
-import { XServiceContext } from "xServices/StateContext"
export const useFeatureVisibility = (): Record => {
- const xServices = useContext(XServiceContext)
- return useSelector(xServices.entitlementsXService, selectFeatureVisibility)
+ const { entitlements } = useDashboard()
+ return selectFeatureVisibility(entitlements)
}
diff --git a/site/src/hooks/useMe.ts b/site/src/hooks/useMe.ts
index c40fa474b9663..00677d66a90e1 100644
--- a/site/src/hooks/useMe.ts
+++ b/site/src/hooks/useMe.ts
@@ -1,12 +1,10 @@
-import { useSelector } from "@xstate/react"
import { User } from "api/typesGenerated"
-import { useContext } from "react"
+import { useAuth } from "components/AuthProvider/AuthProvider"
import { selectUser } from "xServices/auth/authSelectors"
-import { XServiceContext } from "xServices/StateContext"
export const useMe = (): User => {
- const xServices = useContext(XServiceContext)
- const me = useSelector(xServices.authXService, selectUser)
+ const [authState] = useAuth()
+ const me = selectUser(authState)
if (!me) {
throw new Error("User not found.")
diff --git a/site/src/hooks/useOrganizationId.ts b/site/src/hooks/useOrganizationId.ts
index 93712a19dad5b..05c58f4c3a64a 100644
--- a/site/src/hooks/useOrganizationId.ts
+++ b/site/src/hooks/useOrganizationId.ts
@@ -1,11 +1,9 @@
-import { useSelector } from "@xstate/react"
-import { useContext } from "react"
+import { useAuth } from "components/AuthProvider/AuthProvider"
import { selectOrgId } from "../xServices/auth/authSelectors"
-import { XServiceContext } from "../xServices/StateContext"
export const useOrganizationId = (): string => {
- const xServices = useContext(XServiceContext)
- const organizationId = useSelector(xServices.authXService, selectOrgId)
+ const [authState] = useAuth()
+ const organizationId = selectOrgId(authState)
if (!organizationId) {
throw new Error("No organization ID found")
diff --git a/site/src/hooks/usePermissions.ts b/site/src/hooks/usePermissions.ts
index cd0ec0546046b..9b3955c200197 100644
--- a/site/src/hooks/usePermissions.ts
+++ b/site/src/hooks/usePermissions.ts
@@ -1,14 +1,13 @@
-import { useActor } from "@xstate/react"
-import { useContext } from "react"
+import { useAuth } from "components/AuthProvider/AuthProvider"
import { AuthContext } from "xServices/auth/authXService"
-import { XServiceContext } from "xServices/StateContext"
export const usePermissions = (): NonNullable => {
- const xServices = useContext(XServiceContext)
- const [authState, _] = useActor(xServices.authXService)
+ const [authState] = useAuth()
const { permissions } = authState.context
+
if (!permissions) {
throw new Error("Permissions are not loaded yet.")
}
+
return permissions
}
diff --git a/site/src/i18n/en/auditLog.json b/site/src/i18n/en/auditLog.json
index b9b8068d20aaa..d4cd519a688a0 100644
--- a/site/src/i18n/en/auditLog.json
+++ b/site/src/i18n/en/auditLog.json
@@ -15,5 +15,13 @@
"notAvailable": "Not available",
"onBehalfOf": " on behalf of {{owner}}"
}
+ },
+ "paywall": {
+ "title": "Audit logs",
+ "description": "Audit Logs allows Auditors to monitor user operations in their deployment. To use this feature, you have to upgrade your account.",
+ "actions": {
+ "upgrade": "See how to upgrade",
+ "readDocs": "Read the docs"
+ }
}
}
diff --git a/site/src/pages/AuditPage/AuditPage.test.tsx b/site/src/pages/AuditPage/AuditPage.test.tsx
index 490f0a801fef9..3305243739e47 100644
--- a/site/src/pages/AuditPage/AuditPage.test.tsx
+++ b/site/src/pages/AuditPage/AuditPage.test.tsx
@@ -1,26 +1,64 @@
import { screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import * as API from "api/api"
+import { rest } from "msw"
import {
- history,
+ renderWithAuth,
MockAuditLog,
MockAuditLog2,
- render,
waitForLoaderToBeRemoved,
+ MockEntitlementsWithAuditLog,
} from "testHelpers/renderHelpers"
+import { server } from "testHelpers/server"
+
import * as CreateDayString from "util/createDayString"
import AuditPage from "./AuditPage"
+interface RenderPageOptions {
+ filter?: string
+ page?: number
+}
+
+const renderPage = async ({ filter, page }: RenderPageOptions = {}) => {
+ let route = "/audit"
+ const params = new URLSearchParams()
+
+ if (filter) {
+ params.set("filter", filter)
+ }
+
+ if (page) {
+ params.set("page", page.toString())
+ }
+
+ if (Array.from(params).length > 0) {
+ route += `?${params.toString()}`
+ }
+
+ renderWithAuth(, {
+ route,
+ path: "/audit",
+ })
+ await waitForLoaderToBeRemoved()
+}
+
describe("AuditPage", () => {
beforeEach(() => {
// Mocking the dayjs module within the createDayString file
const mock = jest.spyOn(CreateDayString, "createDayString")
mock.mockImplementation(() => "a minute ago")
+
+ // Mock the entitlements
+ server.use(
+ rest.get("/api/v2/entitlements", (req, res, ctx) => {
+ return res(ctx.status(200), ctx.json(MockEntitlementsWithAuditLog))
+ }),
+ )
})
it("shows the audit logs", async () => {
// When
- render()
+ await renderPage()
// Then
await screen.findByTestId(`audit-log-row-${MockAuditLog.id}`)
@@ -29,8 +67,7 @@ describe("AuditPage", () => {
describe("Filtering", () => {
it("filters by typing", async () => {
- render()
- await waitForLoaderToBeRemoved()
+ await renderPage()
await screen.findByText("updated", { exact: false })
const filterField = screen.getByLabelText("Filter")
@@ -47,19 +84,14 @@ describe("AuditPage", () => {
.mockResolvedValue({ audit_logs: [MockAuditLog], count: 1 })
const query = "resource_type:workspace action:create"
- history.push(`/audit?filter=${encodeURIComponent(query)}`)
- render()
-
- await waitForLoaderToBeRemoved()
+ await renderPage({ filter: query })
expect(getAuditLogsSpy).toBeCalledWith({ limit: 25, offset: 0, q: query })
})
it("resets page to 1 when filter is changed", async () => {
- history.push(`/audit?page=2`)
- render()
+ await renderPage({ page: 2 })
- await waitForLoaderToBeRemoved()
const getAuditLogsSpy = jest.spyOn(API, "getAuditLogs")
const filterField = screen.getByLabelText("Filter")
diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx
index ff43d0e065888..cdc6cd0588127 100644
--- a/site/src/pages/AuditPage/AuditPage.tsx
+++ b/site/src/pages/AuditPage/AuditPage.tsx
@@ -3,6 +3,7 @@ import {
getPaginationContext,
nonInitialPage,
} from "components/PaginationWidget/utils"
+import { useFeatureVisibility } from "hooks/useFeatureVisibility"
import { FC } from "react"
import { Helmet } from "react-helmet-async"
import { useSearchParams } from "react-router-dom"
@@ -27,6 +28,7 @@ const AuditPage: FC = () => {
const { auditLogs, count } = auditState.context
const paginationRef = auditState.context.paginationRef as PaginationMachineRef
+ const { audit_log: isAuditLogVisible } = useFeatureVisibility()
return (
<>
@@ -42,6 +44,7 @@ const AuditPage: FC = () => {
}}
paginationRef={paginationRef}
isNonInitialPage={nonInitialPage(searchParams)}
+ isAuditLogVisible={isAuditLogVisible}
/>
>
)
diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx
index a645039e427ac..b7ca7f2d67537 100644
--- a/site/src/pages/AuditPage/AuditPageView.stories.tsx
+++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx
@@ -16,6 +16,9 @@ export default {
paginationRef: {
defaultValue: createPaginationRef({ page: 1, limit: 25 }),
},
+ isAuditLogVisible: {
+ defaultValue: true,
+ },
},
} as ComponentMeta
@@ -45,6 +48,11 @@ NoLogs.args = {
isNonInitialPage: false,
}
+export const NotVisible = Template.bind({})
+NotVisible.args = {
+ isAuditLogVisible: false,
+}
+
export const AuditPageSmallViewport = Template.bind({})
AuditPageSmallViewport.parameters = {
chromatic: { viewports: [600] },
diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx
index 60e731c0082cf..71dc45af77516 100644
--- a/site/src/pages/AuditPage/AuditPageView.tsx
+++ b/site/src/pages/AuditPage/AuditPageView.tsx
@@ -22,6 +22,7 @@ import { AuditHelpTooltip } from "components/Tooltips"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
+import { AuditPaywall } from "./AuditPaywall"
export const Language = {
title: "Audit",
@@ -46,6 +47,7 @@ export interface AuditPageViewProps {
onFilter: (filter: string) => void
paginationRef: PaginationMachineRef
isNonInitialPage: boolean
+ isAuditLogVisible: boolean
}
export const AuditPageView: FC = ({
@@ -55,6 +57,7 @@ export const AuditPageView: FC = ({
onFilter,
paginationRef,
isNonInitialPage,
+ isAuditLogVisible,
}) => {
const { t } = useTranslation("auditLog")
const isLoading = auditLogs === undefined || count === undefined
@@ -72,53 +75,63 @@ export const AuditPageView: FC = ({
{Language.subtitle}
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+ {auditLogs && (
+ new Date(log.time)}
+ row={(log) => (
+
+ )}
+ />
+ )}
-
-
- {auditLogs && (
- new Date(log.time)}
- row={(log) => }
- />
- )}
-
-
-
-
-
+
+
+
+
+
+
-
+
+
+
+
)
}
diff --git a/site/src/pages/AuditPage/AuditPaywall.tsx b/site/src/pages/AuditPage/AuditPaywall.tsx
new file mode 100644
index 0000000000000..d905fc8d6e21a
--- /dev/null
+++ b/site/src/pages/AuditPage/AuditPaywall.tsx
@@ -0,0 +1,40 @@
+import Button from "@material-ui/core/Button"
+import Link from "@material-ui/core/Link"
+import ArrowRightAltOutlined from "@material-ui/icons/ArrowRightAltOutlined"
+import { Paywall } from "components/Paywall/Paywall"
+import { Stack } from "components/Stack/Stack"
+import { FC } from "react"
+import { useTranslation } from "react-i18next"
+
+export const AuditPaywall: FC = () => {
+ const { t } = useTranslation("auditLog")
+
+ return (
+
+
+ }>
+ {t("paywall.actions.upgrade")}
+
+
+
+ {t("paywall.actions.readDocs")}
+
+
+ }
+ />
+ )
+}
diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx
index 6c0b16cddf33b..a39ac1b2e37e0 100644
--- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx
+++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx
@@ -1,9 +1,8 @@
-import { useActor } from "@xstate/react"
import { AppearanceConfig } from "api/typesGenerated"
-import { useContext, FC } from "react"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
+import { FC } from "react"
import { Helmet } from "react-helmet-async"
import { pageTitle } from "util/page"
-import { XServiceContext } from "xServices/StateContext"
import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView"
// ServiceBanner is unlike the other Deployment Settings pages because it
@@ -11,36 +10,23 @@ import { AppearanceSettingsPageView } from "./AppearanceSettingsPageView"
// exception because the Service Banner is visual, and configuring it from
// the command line would be a significantly worse user experience.
const AppearanceSettingsPage: FC = () => {
- const xServices = useContext(XServiceContext)
- const [appearanceXService, appearanceSend] = useActor(
- xServices.appearanceXService,
- )
- const [entitlementsState] = useActor(xServices.entitlementsXService)
- const appearance = appearanceXService.context.appearance
-
+ const { appearance, entitlements } = useDashboard()
const isEntitled =
- entitlementsState.context.entitlements.features["appearance"]
- .entitlement !== "not_entitled"
+ entitlements.features["appearance"].entitlement !== "not_entitled"
const updateAppearance = (
newConfig: Partial,
preview: boolean,
) => {
const newAppearance = {
- ...appearance,
+ ...appearance.config,
...newConfig,
}
if (preview) {
- appearanceSend({
- type: "SET_PREVIEW_APPEARANCE",
- appearance: newAppearance,
- })
+ appearance.setPreview(newAppearance)
return
}
- appearanceSend({
- type: "SET_APPEARANCE",
- appearance: newAppearance,
- })
+ appearance.save(newAppearance)
}
return (
@@ -50,7 +36,7 @@ const AppearanceSettingsPage: FC = () => {
diff --git a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx
index 77300d16fac72..410164fb58448 100644
--- a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx
+++ b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx
@@ -1,15 +1,13 @@
-import { useActor } from "@xstate/react"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"
-import { useContext, FC } from "react"
+import { FC } from "react"
import { Helmet } from "react-helmet-async"
import { pageTitle } from "util/page"
-import { XServiceContext } from "xServices/StateContext"
import { SecuritySettingsPageView } from "./SecuritySettingsPageView"
const SecuritySettingsPage: FC = () => {
const { deploymentConfig: deploymentConfig } = useDeploySettings()
- const xServices = useContext(XServiceContext)
- const [entitlementsState] = useActor(xServices.entitlementsXService)
+ const { entitlements } = useDashboard()
return (
<>
@@ -19,12 +17,9 @@ const SecuritySettingsPage: FC = () => {
>
diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx
index a131a39450a27..fd737b5c4c370 100644
--- a/site/src/pages/LoginPage/LoginPage.tsx
+++ b/site/src/pages/LoginPage/LoginPage.tsx
@@ -1,16 +1,14 @@
-import { useActor } from "@xstate/react"
-import { FC, useContext } from "react"
+import { useAuth } from "components/AuthProvider/AuthProvider"
+import { FC } from "react"
import { Helmet } from "react-helmet-async"
import { useTranslation } from "react-i18next"
import { Navigate, useLocation } from "react-router-dom"
import { retrieveRedirect } from "../../util/redirect"
-import { XServiceContext } from "../../xServices/StateContext"
import { LoginPageView } from "./LoginPageView"
export const LoginPage: FC = () => {
const location = useLocation()
- const xServices = useContext(XServiceContext)
- const [authState, authSend] = useActor(xServices.authXService)
+ const [authState, authSend] = useAuth()
const redirectTo = retrieveRedirect(location.search)
const commonTranslation = useTranslation("common")
const loginPageTranslation = useTranslation("loginPage")
diff --git a/site/src/pages/SetupPage/SetupPage.tsx b/site/src/pages/SetupPage/SetupPage.tsx
index c61a85e137c35..49b55dcdad5e1 100644
--- a/site/src/pages/SetupPage/SetupPage.tsx
+++ b/site/src/pages/SetupPage/SetupPage.tsx
@@ -1,14 +1,13 @@
-import { useActor, useMachine } from "@xstate/react"
-import { FC, useContext, useEffect } from "react"
+import { useMachine } from "@xstate/react"
+import { useAuth } from "components/AuthProvider/AuthProvider"
+import { FC, useEffect } from "react"
import { Helmet } from "react-helmet-async"
import { pageTitle } from "util/page"
import { setupMachine } from "xServices/setup/setupXService"
-import { XServiceContext } from "xServices/StateContext"
import { SetupPageView } from "./SetupPageView"
export const SetupPage: FC = () => {
- const xServices = useContext(XServiceContext)
- const [authState, authSend] = useActor(xServices.authXService)
+ const [authState, authSend] = useAuth()
const [setupState, setupSend] = useMachine(setupMachine, {
actions: {
onCreateFirstUser: ({ firstUser }) => {
diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx
index 1633da6ff9656..b377d94d06b29 100644
--- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx
+++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx
@@ -1,16 +1,14 @@
-import { useActor } from "@xstate/react"
-import { FC, useContext } from "react"
+import { FC } from "react"
import { Section } from "../../../components/SettingsLayout/Section"
import { AccountForm } from "../../../components/SettingsAccountForm/SettingsAccountForm"
-import { XServiceContext } from "../../../xServices/StateContext"
+import { useAuth } from "components/AuthProvider/AuthProvider"
export const Language = {
title: "Account",
}
export const AccountPage: FC = () => {
- const xServices = useContext(XServiceContext)
- const [authState, authSend] = useActor(xServices.authXService)
+ const [authState, authSend] = useAuth()
const { me, permissions, updateProfileError } = authState.context
const canEditUsers = permissions && permissions.updateUsers
diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
index e7ffccadbf181..c6fedc3ffeb13 100644
--- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
+++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx
@@ -113,7 +113,8 @@ describe("WorkspacePage", () => {
const confirmButton = await screen.findByRole("button", { name: "Delete" })
await user.click(confirmButton)
expect(deleteWorkspaceMock).toBeCalled()
- })
+ // This test takes long to finish
+ }, 20_000)
it("requests a start job when the user presses Start", async () => {
server.use(
diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
index 0041901b01897..d81c96944035c 100644
--- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
+++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
@@ -1,6 +1,8 @@
-import { useActor, useSelector } from "@xstate/react"
+import { useActor } from "@xstate/react"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
import dayjs from "dayjs"
-import { useContext, useEffect } from "react"
+import { useFeatureVisibility } from "hooks/useFeatureVisibility"
+import { useEffect } from "react"
import { Helmet } from "react-helmet-async"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
@@ -10,7 +12,6 @@ import {
getMaxDeadlineChange,
getMinDeadline,
} from "util/schedule"
-import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors"
import { quotaMachine } from "xServices/quotas/quotasXService"
import { StateFrom } from "xstate"
import { DeleteDialog } from "../../components/Dialogs/DeleteDialog/DeleteDialog"
@@ -20,7 +21,6 @@ import {
} from "../../components/Workspace/Workspace"
import { pageTitle } from "../../util/page"
import { getFaviconByStatus } from "../../util/workspace"
-import { XServiceContext } from "../../xServices/StateContext"
import {
WorkspaceEvent,
workspaceMachine,
@@ -40,16 +40,8 @@ export const WorkspaceReadyPage = ({
const [_, bannerSend] = useActor(
workspaceState.children["scheduleBannerMachine"],
)
- const xServices = useContext(XServiceContext)
- const experiments = useSelector(
- xServices.experimentsXService,
- (state) => state.context.experiments || [],
- )
- const featureVisibility = useSelector(
- xServices.entitlementsXService,
- selectFeatureVisibility,
- )
- const [buildInfoState] = useActor(xServices.buildInfoXService)
+ const { buildInfo, experiments } = useDashboard()
+ const featureVisibility = useFeatureVisibility()
const {
workspace,
template,
@@ -133,7 +125,7 @@ export const WorkspaceReadyPage = ({
[WorkspaceErrors.BUILD_ERROR]: buildError,
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
}}
- buildInfo={buildInfoState.context.buildInfo}
+ buildInfo={buildInfo}
applicationsHost={applicationsHost}
template={template}
quota_budget={quotaState.context.quota?.budget}
diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx
index 3900ef81479b6..c29f894230e0d 100644
--- a/site/src/testHelpers/renderHelpers.tsx
+++ b/site/src/testHelpers/renderHelpers.tsx
@@ -5,6 +5,8 @@ import {
screen,
waitForElementToBeRemoved,
} from "@testing-library/react"
+import { AuthProvider } from "components/AuthProvider/AuthProvider"
+import { DashboardLayout } from "components/Dashboard/DashboardLayout"
import { createMemoryHistory } from "history"
import { i18n } from "i18n"
import { FC, ReactElement } from "react"
@@ -18,7 +20,6 @@ import {
} from "react-router-dom"
import { RequireAuth } from "../components/RequireAuth/RequireAuth"
import { dark } from "../theme"
-import { XServiceProvider } from "../xServices/StateContext"
import { MockUser } from "./entities"
export const history = createMemoryHistory()
@@ -28,11 +29,11 @@ export const WrapperComponent: FC> = ({
}) => {
return (
-
-
- {children}
-
-
+
+
+ {children}
+
+
)
}
@@ -59,20 +60,22 @@ export function renderWithAuth(
): RenderWithAuthResult {
const renderResult = wrappedRender(
-
-
-
-
+
+
+
+
}>
-
+ }>
+
+
{routes}
-
-
-
-
+
+
+
+
,
)
diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx
deleted file mode 100644
index e7e958e50409c..0000000000000
--- a/site/src/xServices/StateContext.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { useInterpret } from "@xstate/react"
-import { createContext, FC, ReactNode } from "react"
-import { ActorRefFrom } from "xstate"
-import { authMachine } from "./auth/authXService"
-import { buildInfoMachine } from "./buildInfo/buildInfoXService"
-import { entitlementsMachine } from "./entitlements/entitlementsXService"
-import { experimentsMachine } from "./experiments/experimentsMachine"
-import { appearanceMachine } from "./appearance/appearanceXService"
-
-interface XServiceContextType {
- authXService: ActorRefFrom
- buildInfoXService: ActorRefFrom
- entitlementsXService: ActorRefFrom
- experimentsXService: ActorRefFrom
- appearanceXService: ActorRefFrom
-}
-
-/**
- * Consuming this Context will not automatically cause rerenders because
- * the xServices in it are static references.
- *
- * To use one of the xServices, `useActor` will access all its state
- * (causing re-renders for any changes to that one xService) and
- * `useSelector` will access just one piece of state.
- */
-export const XServiceContext = createContext({} as XServiceContextType)
-
-export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => {
- return (
-
- {children}
-
- )
-}
diff --git a/site/src/xServices/appearance/appearanceXService.ts b/site/src/xServices/appearance/appearanceXService.ts
index ae843a5373ccd..8e5ebf507c4d1 100644
--- a/site/src/xServices/appearance/appearanceXService.ts
+++ b/site/src/xServices/appearance/appearanceXService.ts
@@ -4,25 +4,15 @@ import * as API from "../../api/api"
import { AppearanceConfig } from "../../api/typesGenerated"
export type AppearanceContext = {
- appearance: AppearanceConfig
+ appearance?: AppearanceConfig
getAppearanceError?: Error | unknown
setAppearanceError?: Error | unknown
preview: boolean
}
export type AppearanceEvent =
- | {
- type: "GET_APPEARANCE"
- }
| { type: "SET_PREVIEW_APPEARANCE"; appearance: AppearanceConfig }
- | { type: "SET_APPEARANCE"; appearance: AppearanceConfig }
-
-const emptyAppearance: AppearanceConfig = {
- logo_url: "",
- service_banner: {
- enabled: false,
- },
-}
+ | { type: "SAVE_APPEARANCE"; appearance: AppearanceConfig }
export const appearanceMachine = createMachine(
{
@@ -42,16 +32,20 @@ export const appearanceMachine = createMachine(
},
},
context: {
- appearance: emptyAppearance,
preview: false,
},
- initial: "idle",
+ initial: "gettingAppearance",
states: {
idle: {
on: {
- GET_APPEARANCE: "gettingAppearance",
- SET_PREVIEW_APPEARANCE: "settingPreviewAppearance",
- SET_APPEARANCE: "settingAppearance",
+ SET_PREVIEW_APPEARANCE: {
+ actions: [
+ "clearGetAppearanceError",
+ "clearSetAppearanceError",
+ "assignPreviewAppearance",
+ ],
+ },
+ SAVE_APPEARANCE: "savingAppearance",
},
},
gettingAppearance: {
@@ -69,17 +63,7 @@ export const appearanceMachine = createMachine(
},
},
},
- settingPreviewAppearance: {
- entry: [
- "clearGetAppearanceError",
- "clearSetAppearanceError",
- "assignPreviewAppearance",
- ],
- always: {
- target: "idle",
- },
- },
- settingAppearance: {
+ savingAppearance: {
entry: "clearSetAppearanceError",
invoke: {
id: "setAppearance",
@@ -100,16 +84,14 @@ export const appearanceMachine = createMachine(
actions: {
assignPreviewAppearance: assign({
appearance: (_, event) => event.appearance,
- // The xState docs suggest that we can use a static value, but I failed
- // to find a way to do that that doesn't generate type errors.
- preview: (_, __) => true,
+ preview: (_) => true,
}),
notifyUpdateAppearanceSuccess: () => {
displaySuccess("Successfully updated appearance settings!")
},
assignAppearance: assign({
appearance: (_, event) => event.data as AppearanceConfig,
- preview: (_, __) => false,
+ preview: (_) => false,
}),
assignGetAppearanceError: assign({
getAppearanceError: (_, event) => event.data,
diff --git a/site/src/xServices/auth/authSelectors.ts b/site/src/xServices/auth/authSelectors.ts
index 7c39442ac93ae..60848d9489317 100644
--- a/site/src/xServices/auth/authSelectors.ts
+++ b/site/src/xServices/auth/authSelectors.ts
@@ -1,7 +1,7 @@
-import { State } from "xstate"
-import { AuthContext, AuthEvent } from "./authXService"
+import { StateFrom } from "xstate"
+import { AuthContext, authMachine } from "./authXService"
-type AuthState = State
+type AuthState = StateFrom
export const selectOrgId = (state: AuthState): string | undefined => {
return state.context.me?.organization_ids[0]
diff --git a/site/src/xServices/entitlements/entitlementsSelectors.ts b/site/src/xServices/entitlements/entitlementsSelectors.ts
index b634a2a25ed0c..5777700604625 100644
--- a/site/src/xServices/entitlements/entitlementsSelectors.ts
+++ b/site/src/xServices/entitlements/entitlementsSelectors.ts
@@ -1,8 +1,4 @@
-import { Feature, FeatureName } from "api/typesGenerated"
-import { State } from "xstate"
-import { EntitlementsContext, EntitlementsEvent } from "./entitlementsXService"
-
-type EntitlementState = State
+import { Entitlements, Feature, FeatureName } from "api/typesGenerated"
/**
* @param hasLicense true if Enterprise edition
@@ -27,10 +23,7 @@ export const getFeatureVisibility = (
}
export const selectFeatureVisibility = (
- state: EntitlementState,
+ entitlements: Entitlements,
): Record => {
- return getFeatureVisibility(
- state.context.entitlements.has_license,
- state.context.entitlements.features,
- )
+ return getFeatureVisibility(entitlements.has_license, entitlements.features)
}
diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts
index 96b175897c70b..783bacc6c8358 100644
--- a/site/src/xServices/entitlements/entitlementsXService.ts
+++ b/site/src/xServices/entitlements/entitlementsXService.ts
@@ -1,31 +1,12 @@
-import { withDefaultFeatures } from "./../../api/api"
-import { MockEntitlementsWithWarnings } from "testHelpers/entities"
import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import { Entitlements } from "../../api/typesGenerated"
export type EntitlementsContext = {
- entitlements: Entitlements
+ entitlements?: Entitlements
getEntitlementsError?: Error | unknown
}
-export type EntitlementsEvent =
- | {
- type: "GET_ENTITLEMENTS"
- }
- | { type: "SHOW_MOCK_BANNER" }
- | { type: "HIDE_MOCK_BANNER" }
-
-const emptyEntitlements = {
- errors: [],
- warnings: [],
- features: withDefaultFeatures({}),
- has_license: false,
- experimental: false,
- experimental_features: [],
- trial: false,
-}
-
export const entitlementsMachine = createMachine(
{
id: "entitlementsMachine",
@@ -33,40 +14,35 @@ export const entitlementsMachine = createMachine(
tsTypes: {} as import("./entitlementsXService.typegen").Typegen0,
schema: {
context: {} as EntitlementsContext,
- events: {} as EntitlementsEvent,
services: {
getEntitlements: {
data: {} as Entitlements,
},
},
},
- context: {
- entitlements: emptyEntitlements,
- },
- initial: "idle",
+ initial: "gettingEntitlements",
states: {
- idle: {
- on: {
- GET_ENTITLEMENTS: "gettingEntitlements",
- SHOW_MOCK_BANNER: { actions: "assignMockEntitlements" },
- HIDE_MOCK_BANNER: "gettingEntitlements",
- },
- },
gettingEntitlements: {
entry: "clearGetEntitlementsError",
invoke: {
id: "getEntitlements",
src: "getEntitlements",
onDone: {
- target: "idle",
+ target: "success",
actions: ["assignEntitlements"],
},
onError: {
- target: "idle",
+ target: "error",
actions: ["assignGetEntitlementsError"],
},
},
},
+ success: {
+ type: "final",
+ },
+ error: {
+ type: "final",
+ },
},
},
{
@@ -80,9 +56,6 @@ export const entitlementsMachine = createMachine(
clearGetEntitlementsError: assign({
getEntitlementsError: (_) => undefined,
}),
- assignMockEntitlements: assign({
- entitlements: (_) => MockEntitlementsWithWarnings,
- }),
},
services: {
getEntitlements: API.getEntitlements,
diff --git a/site/src/xServices/experiments/experimentsMachine.ts b/site/src/xServices/experiments/experimentsMachine.ts
index 25acab364bf0b..399eaf6c2650b 100644
--- a/site/src/xServices/experiments/experimentsMachine.ts
+++ b/site/src/xServices/experiments/experimentsMachine.ts
@@ -20,9 +20,6 @@ export const experimentsMachine = createMachine(
}
},
},
- context: {
- experiments: undefined,
- },
initial: "gettingExperiments",
states: {
gettingExperiments: {