From 244845f17647fb174002547a2d72d14475ef6a0e Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 18 Jan 2023 18:20:57 +0000 Subject: [PATCH 01/16] Add paywall to the audit route --- site/src/AppRouter.tsx | 30 +---- site/src/pages/AuditPage/AuditPage.tsx | 3 + site/src/pages/AuditPage/AuditPageView.tsx | 121 ++++++++++++++------- 3 files changed, 86 insertions(+), 68 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 94ecbd591343e..4e9e5a9c1b53e 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,11 +10,8 @@ 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 { FC, lazy, Suspense } 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 { RequireAuth } from "./components/RequireAuth/RequireAuth" import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout" @@ -123,13 +118,6 @@ const CreateTemplatePage = lazy( ) export const AppRouter: FC = () => { - const xServices = useContext(XServiceContext) - const permissions = useSelector(xServices.authXService, selectPermissions) - const featureVisibility = useSelector( - xServices.entitlementsXService, - selectFeatureVisibility, - ) - return ( }> @@ -188,21 +176,7 @@ export const AppRouter: 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.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 60e731c0082cf..6ee312e1bd7d9 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -1,3 +1,5 @@ +import Button from "@material-ui/core/Button" +import Link from "@material-ui/core/Link" import Table from "@material-ui/core/Table" import TableBody from "@material-ui/core/TableBody" import TableCell from "@material-ui/core/TableCell" @@ -8,12 +10,14 @@ import { AuditLogRow } from "components/AuditLogRow/AuditLogRow" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { EmptyState } from "components/EmptyState/EmptyState" import { Margins } from "components/Margins/Margins" +import ArrowRightAltOutlined from "@material-ui/icons/ArrowRightAltOutlined" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, } from "components/PageHeader/PageHeader" import { PaginationWidget } from "components/PaginationWidget/PaginationWidget" +import { Paywall } from "components/Paywall/Paywall" import { SearchBarWithFilter } from "components/SearchBarWithFilter/SearchBarWithFilter" import { Stack } from "components/Stack/Stack" import { TableLoader } from "components/TableLoader/TableLoader" @@ -46,6 +50,7 @@ export interface AuditPageViewProps { onFilter: (filter: string) => void paginationRef: PaginationMachineRef isNonInitialPage: boolean + isAuditLogVisible: boolean } export const AuditPageView: FC = ({ @@ -55,6 +60,7 @@ export const AuditPageView: FC = ({ onFilter, paginationRef, isNonInitialPage, + isAuditLogVisible, }) => { const { t } = useTranslation("auditLog") const isLoading = auditLogs === undefined || count === undefined @@ -72,53 +78,88 @@ export const AuditPageView: FC = ({ {Language.subtitle} - + + + - - - - - - - - + +
+ - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + {auditLogs && ( + new Date(log.time)} + row={(log) => ( + + )} + /> + )} - - - {auditLogs && ( - new Date(log.time)} - row={(log) => } - /> - )} - - - -
-
+ + + + + +
- + + + + + + + Read the docs + + + } + /> + +
) } From b83f9c2c28e7b7e1dd6825c9a05a68315ba60350 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 18 Jan 2023 19:36:15 +0000 Subject: [PATCH 02/16] refactor auth provider --- site/src/AppRouter.tsx | 30 ++-------------- site/src/app.tsx | 6 ++-- .../components/AuthProvider/AuthProvider.tsx | 36 +++++++++++++++++++ site/src/components/Navbar/Navbar.tsx | 12 ++++--- .../components/RequireAuth/RequireAuth.tsx | 8 ++--- .../ServiceBanner/ServiceBanner.tsx | 3 +- .../SettingsAccountForm.test.tsx | 2 +- .../TemplateLayout/TemplateLayout.tsx | 8 ++--- site/src/hooks/useMe.ts | 8 ++--- site/src/hooks/useOrganizationId.ts | 8 ++--- site/src/hooks/usePermissions.ts | 9 +++-- site/src/pages/LoginPage/LoginPage.tsx | 8 ++--- site/src/pages/SetupPage/SetupPage.tsx | 2 +- .../AccountPage/AccountPage.tsx | 8 ++--- site/src/xServices/StateContext.tsx | 3 -- site/src/xServices/auth/authSelectors.ts | 6 ++-- 16 files changed, 78 insertions(+), 79 deletions(-) create mode 100644 site/src/components/AuthProvider/AuthProvider.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 94ecbd591343e..4e9e5a9c1b53e 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,11 +10,8 @@ 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 { FC, lazy, Suspense } 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 { RequireAuth } from "./components/RequireAuth/RequireAuth" import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout" @@ -123,13 +118,6 @@ const CreateTemplatePage = lazy( ) export const AppRouter: FC = () => { - const xServices = useContext(XServiceContext) - const permissions = useSelector(xServices.authXService, selectPermissions) - const featureVisibility = useSelector( - xServices.entitlementsXService, - selectFeatureVisibility, - ) - return ( }> @@ -188,21 +176,7 @@ export const AppRouter: FC = () => { } /> - - - - - } - /> - + } /> { return ( @@ -17,10 +17,10 @@ export const App: FC = () => { - + - + diff --git a/site/src/components/AuthProvider/AuthProvider.tsx b/site/src/components/AuthProvider/AuthProvider.tsx new file mode 100644 index 0000000000000..6120d04418a61 --- /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 AuthProviderContextValue { + authService: ActorRefFrom +} + +const AuthProviderContext = createContext( + undefined, +) + +export const AuthProvider: FC = ({ children }) => { + const authService = useInterpret(authMachine) + + return ( + + {children} + + ) +} + +// The returned type is kinda complex to rewrite it +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -- Read above +export const useAuth = () => { + const context = useContext(AuthProviderContext) + + if (!context) { + throw new Error("useAuth should be used inside of ") + } + + const auth = useActor(context.authService) + + return auth +} diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index 7941a95b1b92a..8f2195c772f26 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -1,4 +1,7 @@ import { shallowEqual, useActor, useSelector } from "@xstate/react" +import { useAuth } from "components/AuthProvider/AuthProvider" +import { useMe } from "hooks/useMe" +import { usePermissions } from "hooks/usePermissions" import { useContext, FC } from "react" import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { XServiceContext } from "../../xServices/StateContext" @@ -7,17 +10,18 @@ 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 [_, authSend] = useAuth() + const me = useMe() + const permissions = usePermissions() const featureVisibility = useSelector( xServices.entitlementsXService, selectFeatureVisibility, shallowEqual, ) 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 ( diff --git a/site/src/components/RequireAuth/RequireAuth.tsx b/site/src/components/RequireAuth/RequireAuth.tsx index d4f9ce22e6a5c..a4716b517fec8 100644 --- a/site/src/components/RequireAuth/RequireAuth.tsx +++ b/site/src/components/RequireAuth/RequireAuth.tsx @@ -1,14 +1,12 @@ -import { useActor } from "@xstate/react" -import { useContext, FC } from "react" +import { useAuth } from "components/AuthProvider/AuthProvider" +import { FC } from "react" import { Navigate, useLocation } from "react-router" import { Outlet } from "react-router-dom" import { embedRedirect } from "../../util/redirect" -import { XServiceContext } from "../../xServices/StateContext" import { FullScreenLoader } from "../Loader/FullScreenLoader" export const RequireAuth: FC = () => { - 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..dc921e83179dc 100644 --- a/site/src/components/ServiceBanner/ServiceBanner.tsx +++ b/site/src/components/ServiceBanner/ServiceBanner.tsx @@ -1,4 +1,5 @@ import { useActor } from "@xstate/react" +import { useAuth } from "components/AuthProvider/AuthProvider" import { useContext, useEffect } from "react" import { XServiceContext } from "xServices/StateContext" import { ServiceBannerView } from "./ServiceBannerView" @@ -8,7 +9,7 @@ export const ServiceBanner: React.FC = () => { const [appearanceState, appearanceSend] = useActor( xServices.appearanceXService, ) - const [authState] = useActor(xServices.authXService) + const [authState] = useAuth() const { message, background_color, enabled } = appearanceState.context.appearance.service_banner 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/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/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..5e3831775c349 100644 --- a/site/src/pages/SetupPage/SetupPage.tsx +++ b/site/src/pages/SetupPage/SetupPage.tsx @@ -8,7 +8,7 @@ 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/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index d77c94defb09e..60f3acc42eacc 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -1,13 +1,11 @@ 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 { appearanceMachine } from "./appearance/appearanceXService" interface XServiceContextType { - authXService: ActorRefFrom buildInfoXService: ActorRefFrom entitlementsXService: ActorRefFrom appearanceXService: ActorRefFrom @@ -27,7 +25,6 @@ export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => { return ( +type AuthState = StateFrom export const selectOrgId = (state: AuthState): string | undefined => { return state.context.me?.organization_ids[0] From 528892f130e68e1dc49fe105bac7837f1641d83c Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 18 Jan 2023 19:39:44 +0000 Subject: [PATCH 03/16] Move router to be inside of AppRouter --- site/src/AppRouter.tsx | 175 +++++++++++++++++++++-------------------- site/src/app.tsx | 25 +++--- 2 files changed, 101 insertions(+), 99 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 4e9e5a9c1b53e..a6abbd4b08232 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -11,7 +11,7 @@ import TemplatesPage from "pages/TemplatesPage/TemplatesPage" import UsersPage from "pages/UsersPage/UsersPage" import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage" import { FC, lazy, Suspense } from "react" -import { Route, Routes } from "react-router-dom" +import { Route, Routes, BrowserRouter as Router } from "react-router-dom" import { DashboardLayout } from "./components/DashboardLayout/DashboardLayout" import { RequireAuth } from "./components/RequireAuth/RequireAuth" import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout" @@ -120,112 +120,117 @@ const CreateTemplatePage = lazy( export const AppRouter: FC = () => { return ( }> - - } /> - } /> + + + } /> + } /> - {/* Dashboard routes */} - }> - }> - } /> + {/* Dashboard routes */} + }> + }> + } /> - } /> + } /> - } /> + } /> - - } /> - } /> - + + } /> + } /> + - - } /> - } /> - - }> - } /> - } - /> + + } /> + } /> + + }> + } /> + } + /> + + + } /> + } /> + + } /> + + - } /> - } /> - - } /> + + }> + } /> - - - - }> - } /> + } /> - } /> - + + }> + } /> + - - }> - } /> + } /> + } /> + } + /> - } /> - } /> - } /> - - - } /> - - } - > - } /> - } /> - } /> - } /> - } /> - } /> - + } /> + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + - }> - } /> - } /> - } /> - } /> - + }> + } /> + } /> + } /> + } /> + - - - } /> - } /> - } - /> - } - /> + + + } /> + } /> + } + /> + } + /> + - - {/* 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/app.tsx b/site/src/app.tsx index d4011857670da..5dbc2d5e7fcd5 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -3,7 +3,6 @@ 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" @@ -12,18 +11,16 @@ import "./theme/globalFonts" export const App: FC = () => { return ( - - - - - - - - - - - - - + + + + + + + + + + + ) } From 9f0e0eeede9c0715a8c54b2889ef0967a6880cf7 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 18 Jan 2023 23:23:26 +0000 Subject: [PATCH 04/16] Create dashboard provider --- site/src/AppRouter.tsx | 2 +- .../DashboardLayout.tsx | 5 +- .../Dashboard/DashboardProvider.tsx | 83 +++++++++++++++++++ .../LicenseBanner/LicenseBanner.tsx | 16 +--- site/src/components/Navbar/Navbar.tsx | 21 ++--- .../ServiceBanner/ServiceBanner.tsx | 21 +---- site/src/hooks/useFeatureVisibility.ts | 8 +- .../AppearanceSettingsPage.tsx | 30 ++----- .../SecuritySettingsPage.tsx | 15 ++-- site/src/pages/SetupPage/SetupPage.tsx | 7 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 23 ++--- site/src/testHelpers/renderHelpers.tsx | 30 ++++--- site/src/xServices/StateContext.tsx | 36 -------- .../appearance/appearanceXService.ts | 40 +++------ .../entitlements/entitlementsSelectors.ts | 13 +-- .../entitlements/entitlementsXService.ts | 46 +++------- 16 files changed, 167 insertions(+), 229 deletions(-) rename site/src/components/{DashboardLayout => Dashboard}/DashboardLayout.tsx (95%) create mode 100644 site/src/components/Dashboard/DashboardProvider.tsx delete mode 100644 site/src/xServices/StateContext.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index a6abbd4b08232..715a4b080bb96 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -12,7 +12,7 @@ import UsersPage from "pages/UsersPage/UsersPage" import WorkspacesPage from "pages/WorkspacesPage/WorkspacesPage" import { FC, lazy, Suspense } from "react" import { Route, Routes, BrowserRouter as Router } from "react-router-dom" -import { DashboardLayout } from "./components/DashboardLayout/DashboardLayout" +import { DashboardLayout } from "./components/Dashboard/DashboardLayout" import { RequireAuth } from "./components/RequireAuth/RequireAuth" import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout" import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout" 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..cc43ad6134028 --- /dev/null +++ b/site/src/components/Dashboard/DashboardProvider.tsx @@ -0,0 +1,83 @@ +import { useMachine } from "@xstate/react" +import { + AppearanceConfig, + BuildInfoResponse, + Entitlements, +} 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" + +interface Appearance { + config: AppearanceConfig + preview: boolean + setPreview: (config: AppearanceConfig) => void + save: (config: AppearanceConfig) => void +} + +interface DashboardProviderValue { + buildInfo: BuildInfoResponse + entitlements: Entitlements + appearance: Appearance +} + +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 { buildInfo } = buildInfoState.context + const { entitlements } = entitlementsState.context + const { appearance, preview } = appearanceState.context + const isLoading = !buildInfo || !entitlements || !appearance + + 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.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 8f2195c772f26..c7090178f2d5d 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -1,24 +1,17 @@ -import { shallowEqual, useActor, useSelector } from "@xstate/react" 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 { useContext, FC } from "react" -import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" -import { XServiceContext } from "../../xServices/StateContext" +import { FC } from "react" import { NavbarView } from "../NavbarView/NavbarView" export const Navbar: FC = () => { - const xServices = useContext(XServiceContext) - const [appearanceState] = useActor(xServices.appearanceXService) - const [buildInfoState] = useActor(xServices.buildInfoXService) + const { appearance, buildInfo } = useDashboard() const [_, authSend] = useAuth() const me = useMe() const permissions = usePermissions() - const featureVisibility = useSelector( - xServices.entitlementsXService, - selectFeatureVisibility, - shallowEqual, - ) + const featureVisibility = useFeatureVisibility() const canViewAuditLog = featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog) const canViewDeployment = Boolean(permissions.viewDeploymentConfig) @@ -27,8 +20,8 @@ export const Navbar: FC = () => { return ( { - const xServices = useContext(XServiceContext) - const [appearanceState, appearanceSend] = useActor( - xServices.appearanceXService, - ) - const [authState] = useAuth() + 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 @@ -28,7 +15,7 @@ export const ServiceBanner: React.FC = () => { ) } else { 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/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/SetupPage/SetupPage.tsx b/site/src/pages/SetupPage/SetupPage.tsx index 5e3831775c349..49b55dcdad5e1 100644 --- a/site/src/pages/SetupPage/SetupPage.tsx +++ b/site/src/pages/SetupPage/SetupPage.tsx @@ -1,13 +1,12 @@ -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] = useAuth() const [setupState, setupSend] = useMachine(setupMachine, { actions: { diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 3c8f6324ac35b..83febfd56db10 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,9 @@ export const WorkspaceReadyPage = ({ const [_, bannerSend] = useActor( workspaceState.children["scheduleBannerMachine"], ) - const xServices = useContext(XServiceContext) - const experimental = useSelector( - xServices.entitlementsXService, - (state) => state.context.entitlements.experimental, - ) - const featureVisibility = useSelector( - xServices.entitlementsXService, - selectFeatureVisibility, - ) - const [buildInfoState] = useActor(xServices.buildInfoXService) + const { entitlements, buildInfo } = useDashboard() + const experimental = entitlements.experimental + const featureVisibility = useFeatureVisibility() const { workspace, template, @@ -132,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..421eadfc2ff95 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -5,6 +5,7 @@ import { screen, waitForElementToBeRemoved, } from "@testing-library/react" +import { DashboardLayout } from "components/Dashboard/DashboardLayout" import { createMemoryHistory } from "history" import { i18n } from "i18n" import { FC, ReactElement } from "react" @@ -18,7 +19,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() @@ -29,9 +29,7 @@ export const WrapperComponent: FC> = ({ return ( - - {children} - + {children} ) @@ -59,20 +57,20 @@ export function renderWithAuth( ): RenderWithAuthResult { const renderResult = wrappedRender( - - - - - - }> + + + + + }> + }> - {routes} - - - - - +
+ {routes} +
+ + + , ) diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx deleted file mode 100644 index 60f3acc42eacc..0000000000000 --- a/site/src/xServices/StateContext.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useInterpret } from "@xstate/react" -import { createContext, FC, ReactNode } from "react" -import { ActorRefFrom } from "xstate" -import { buildInfoMachine } from "./buildInfo/buildInfoXService" -import { entitlementsMachine } from "./entitlements/entitlementsXService" -import { appearanceMachine } from "./appearance/appearanceXService" - -interface XServiceContextType { - buildInfoXService: ActorRefFrom - entitlementsXService: 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..7f245abcdf71d 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", 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 52bceac1b2967..783bacc6c8358 100644 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ b/site/src/xServices/entitlements/entitlementsXService.ts @@ -1,30 +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, - trial: false, -} - export const entitlementsMachine = createMachine( { id: "entitlementsMachine", @@ -32,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", + }, }, }, { @@ -79,9 +56,6 @@ export const entitlementsMachine = createMachine( clearGetEntitlementsError: assign({ getEntitlementsError: (_) => undefined, }), - assignMockEntitlements: assign({ - entitlements: (_) => MockEntitlementsWithWarnings, - }), }, services: { getEntitlements: API.getEntitlements, From 1714ca51931cfdeacb5949ddcf4bdd90b18a4ddb Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 18 Jan 2023 23:25:16 +0000 Subject: [PATCH 05/16] Minor refactoring --- site/src/xServices/appearance/appearanceXService.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/site/src/xServices/appearance/appearanceXService.ts b/site/src/xServices/appearance/appearanceXService.ts index 7f245abcdf71d..8e5ebf507c4d1 100644 --- a/site/src/xServices/appearance/appearanceXService.ts +++ b/site/src/xServices/appearance/appearanceXService.ts @@ -84,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, From 884dac7e523910c33ef46f1f6c849c57323013e5 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 18 Jan 2023 23:45:15 +0000 Subject: [PATCH 06/16] Fix tests --- .../LicenseBanner/LicenseBanner.test.tsx | 27 -------------- .../WorkspaceStats/WorkspaceStats.stories.tsx | 9 +++++ .../WorkspaceStats/WorkspaceStats.test.tsx | 36 ------------------- site/src/testHelpers/renderHelpers.tsx | 29 ++++++++------- 4 files changed, 26 insertions(+), 75 deletions(-) delete mode 100644 site/src/components/LicenseBanner/LicenseBanner.test.tsx delete mode 100644 site/src/components/WorkspaceStats/WorkspaceStats.test.tsx 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/WorkspaceStats/WorkspaceStats.stories.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx index e2845052c695d..699e5cc07ab5b 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/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index 421eadfc2ff95..c29f894230e0d 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -5,6 +5,7 @@ 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" @@ -28,9 +29,11 @@ export const WrapperComponent: FC> = ({ }) => { return ( - - {children} - + + + {children} + + ) } @@ -59,16 +62,18 @@ export function renderWithAuth( - - - }> - }> - + + + + }> + }> + + - - {routes} - - + {routes} + + + , From 6442b3a93925e723b5d67d7ca12eea478989b716 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 13:34:27 +0000 Subject: [PATCH 07/16] Fix Audit Log page test --- site/src/api/api.ts | 23 +-------- site/src/pages/AuditPage/AuditPage.test.tsx | 56 ++++++++++++++++----- 2 files changed, 46 insertions(+), 33 deletions(-) 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/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") From 5747590cd71d6e8812ace2f73f728d2cda3a98c2 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 13:57:02 +0000 Subject: [PATCH 08/16] Add long time running test --- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index e7ffccadbf181..73de4b885ee78 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -89,6 +89,8 @@ afterAll(() => { describe("WorkspacePage", () => { it("requests a delete job when the user presses Delete and confirms", async () => { + // This is a long running test + jest.setTimeout(20_000) const user = userEvent.setup() const deleteWorkspaceMock = jest From 7768e95162c5f026402ed5cda62c51b136785d67 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 13:59:24 +0000 Subject: [PATCH 09/16] Fix stories --- site/src/pages/AuditPage/AuditPageView.stories.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) 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] }, From 1173d1636c1aa17101da75a722cc08a161eeacd8 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 14:10:05 +0000 Subject: [PATCH 10/16] Increase timeout --- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 73de4b885ee78..c4f76cdda6bd5 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -81,6 +81,7 @@ beforeAll(() => { beforeEach(() => { jest.resetAllMocks() + jest.setTimeout(20_000) }) afterAll(() => { @@ -89,8 +90,6 @@ afterAll(() => { describe("WorkspacePage", () => { it("requests a delete job when the user presses Delete and confirms", async () => { - // This is a long running test - jest.setTimeout(20_000) const user = userEvent.setup() const deleteWorkspaceMock = jest From ea4300bd06c47489ba2f914acd783fc29f822dfc Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 14:11:03 +0000 Subject: [PATCH 11/16] Increse timeout --- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index c4f76cdda6bd5..c6fedc3ffeb13 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -81,7 +81,6 @@ beforeAll(() => { beforeEach(() => { jest.resetAllMocks() - jest.setTimeout(20_000) }) afterAll(() => { @@ -114,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( From d8826c32b602d2815d7ce1d8a2182b7ef352893e Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 13:54:29 -0300 Subject: [PATCH 12/16] Update site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx Co-authored-by: Kira Pilot --- site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx index 699e5cc07ab5b..dff7b7ce6f567 100644 --- a/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceStats.stories.tsx @@ -1,6 +1,6 @@ import { Story } from "@storybook/react" import * as Mocks from "../../testHelpers/renderHelpers" -import { MockWorkspace } from "../../testHelpers/renderHelpers" +import { MockWorkspace } from "testHelpers/renderHelpers" import { WorkspaceStats, WorkspaceStatsProps, From e12f437ab893bf953163101709d7d89c2d124419 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 14:06:41 -0300 Subject: [PATCH 13/16] Update site/src/components/AuthProvider/AuthProvider.tsx Co-authored-by: Joe Previte --- site/src/components/AuthProvider/AuthProvider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/components/AuthProvider/AuthProvider.tsx b/site/src/components/AuthProvider/AuthProvider.tsx index 6120d04418a61..4f035846db336 100644 --- a/site/src/components/AuthProvider/AuthProvider.tsx +++ b/site/src/components/AuthProvider/AuthProvider.tsx @@ -21,9 +21,9 @@ export const AuthProvider: FC = ({ children }) => { ) } -// The returned type is kinda complex to rewrite it -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -- Read above -export const useAuth = () => { +type UseAuthReturnType = ReturnType> + +export const useAuth = (): UseAuthReturnType => { const context = useContext(AuthProviderContext) if (!context) { From 9e7a875e04a9e1ca01ee552752a809f90661184c Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 23:41:33 +0000 Subject: [PATCH 14/16] Fix format --- site/src/components/AuthProvider/AuthProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/components/AuthProvider/AuthProvider.tsx b/site/src/components/AuthProvider/AuthProvider.tsx index 4f035846db336..c115e65541d00 100644 --- a/site/src/components/AuthProvider/AuthProvider.tsx +++ b/site/src/components/AuthProvider/AuthProvider.tsx @@ -21,7 +21,9 @@ export const AuthProvider: FC = ({ children }) => { ) } -type UseAuthReturnType = ReturnType> +type UseAuthReturnType = ReturnType< + typeof useActor +> export const useAuth = (): UseAuthReturnType => { const context = useContext(AuthProviderContext) From 3e589f1434304e7005bb594dc73a8caa8cd1ba93 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 23:44:57 +0000 Subject: [PATCH 15/16] Improve name --- site/src/components/AuthProvider/AuthProvider.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/site/src/components/AuthProvider/AuthProvider.tsx b/site/src/components/AuthProvider/AuthProvider.tsx index c115e65541d00..1b113e80fc155 100644 --- a/site/src/components/AuthProvider/AuthProvider.tsx +++ b/site/src/components/AuthProvider/AuthProvider.tsx @@ -3,30 +3,28 @@ import { createContext, FC, PropsWithChildren, useContext } from "react" import { authMachine } from "xServices/auth/authXService" import { ActorRefFrom } from "xstate" -interface AuthProviderContextValue { +interface AuthContextValue { authService: ActorRefFrom } -const AuthProviderContext = createContext( - undefined, -) +const AuthContext = createContext(undefined) export const AuthProvider: FC = ({ children }) => { const authService = useInterpret(authMachine) return ( - + {children} - + ) } type UseAuthReturnType = ReturnType< - typeof useActor + typeof useActor > export const useAuth = (): UseAuthReturnType => { - const context = useContext(AuthProviderContext) + const context = useContext(AuthContext) if (!context) { throw new Error("useAuth should be used inside of ") From dce50e381e55e7009fa7e7794c521ccd2b27b6dc Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 19 Jan 2023 23:51:09 +0000 Subject: [PATCH 16/16] Extract audit log paywall --- site/src/i18n/en/auditLog.json | 8 +++++ site/src/pages/AuditPage/AuditPageView.tsx | 32 ++--------------- site/src/pages/AuditPage/AuditPaywall.tsx | 40 ++++++++++++++++++++++ 3 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 site/src/pages/AuditPage/AuditPaywall.tsx 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/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 6ee312e1bd7d9..71dc45af77516 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -1,5 +1,3 @@ -import Button from "@material-ui/core/Button" -import Link from "@material-ui/core/Link" import Table from "@material-ui/core/Table" import TableBody from "@material-ui/core/TableBody" import TableCell from "@material-ui/core/TableCell" @@ -10,14 +8,12 @@ import { AuditLogRow } from "components/AuditLogRow/AuditLogRow" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { EmptyState } from "components/EmptyState/EmptyState" import { Margins } from "components/Margins/Margins" -import ArrowRightAltOutlined from "@material-ui/icons/ArrowRightAltOutlined" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle, } from "components/PageHeader/PageHeader" import { PaginationWidget } from "components/PaginationWidget/PaginationWidget" -import { Paywall } from "components/Paywall/Paywall" import { SearchBarWithFilter } from "components/SearchBarWithFilter/SearchBarWithFilter" import { Stack } from "components/Stack/Stack" import { TableLoader } from "components/TableLoader/TableLoader" @@ -26,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", @@ -132,32 +129,7 @@ export const AuditPageView: FC = ({ - - - - - - Read the docs - - - } - /> + 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")} + + + } + /> + ) +}