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.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: {