From 31ae8eb434d6e9155bae7a774386a58fb121a3bd Mon Sep 17 00:00:00 2001 From: presleyp Date: Fri, 15 Jul 2022 18:00:04 +0000 Subject: [PATCH 1/7] Add stubbed api call --- site/src/api/api.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8517dab9e8d26..b2a4ee8901f5a 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -68,6 +68,29 @@ export const checkUserPermissions = async ( return response.data } +export const getLicenseData = async (): Promise => { + const fakeLicenseData = { + features: { + audit: { + entitled: false, + enabled: true + }, + createUser: { + entitled: true, + enabled: true, + limit: 1, + actual: 2 + }, + createOrg: { + entitled: true, + enabled: false + } + }, + warnings: ["This is a test license compliance banner", "Here is a second one"] + } + return Promise.resolve(fakeLicenseData) +} + export const getApiKey = async (): Promise => { const response = await axios.post("/api/v2/users/me/keys") return response.data From ead0ab44619bfff1d968916b67d44fc6ede9fbe5 Mon Sep 17 00:00:00 2001 From: presleyp Date: Fri, 15 Jul 2022 18:01:06 +0000 Subject: [PATCH 2/7] Add licenseXService --- site/src/api/types.ts | 14 +++ site/src/xServices/StateContext.tsx | 3 + .../src/xServices/license/licenseSelectors.ts | 24 +++++ site/src/xServices/license/licenseXService.ts | 94 +++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 site/src/xServices/license/licenseSelectors.ts create mode 100644 site/src/xServices/license/licenseXService.ts diff --git a/site/src/api/types.ts b/site/src/api/types.ts index daf4e451ac5e8..340d1c078c092 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -14,3 +14,17 @@ export interface ReconnectingPTYRequest { export type WorkspaceBuildTransition = "start" | "stop" | "delete" export type Message = { message: string } + +export type LicensePermission = "audit" | "createUser" | "createOrg" + +export type LicenseFeatures = Record + +export type LicenseData = { + features: LicenseFeatures + warnings: string[] +} diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index c9628cfe2608e..bc5ce8083bc8f 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -4,6 +4,7 @@ import { useNavigate } from "react-router" import { ActorRefFrom } from "xstate" import { authMachine } from "./auth/authXService" import { buildInfoMachine } from "./buildInfo/buildInfoXService" +import { licenseMachine } from "./license/licenseXService" import { siteRolesMachine } from "./roles/siteRolesXService" import { usersMachine } from "./users/usersXService" @@ -12,6 +13,7 @@ interface XServiceContextType { buildInfoXService: ActorRefFrom usersXService: ActorRefFrom siteRolesXService: ActorRefFrom + licenseXService: ActorRefFrom } /** @@ -39,6 +41,7 @@ export const XServiceProvider: React.FC = ({ children }) => { usersMachine.withConfig({ actions: { redirectToUsersPage } }), ), siteRolesXService: useInterpret(siteRolesMachine), + licenseXService: useInterpret(licenseMachine) }} > {children} diff --git a/site/src/xServices/license/licenseSelectors.ts b/site/src/xServices/license/licenseSelectors.ts new file mode 100644 index 0000000000000..378e506622d11 --- /dev/null +++ b/site/src/xServices/license/licenseSelectors.ts @@ -0,0 +1,24 @@ +import { State } from "xstate" +import { LicensePermission } from "../../api/types" +import { LicenseContext, LicenseEvent } from "./licenseXService" +type LicenseState = State + +export const selectLicenseVisibility = (state: LicenseState): Record => { + const features = state.context.licenseData.features + const featureNames = Object.keys(features) as LicensePermission[] + const visibilityPairs = featureNames.map((feature: LicensePermission) => { + return [feature, features[feature].enabled] + }) + return Object.fromEntries(visibilityPairs) +} + +export const selectLicenseEntitlement = (state: LicenseState): Record => { + const features = state.context.licenseData.features + const featureNames = Object.keys(features) as LicensePermission[] + const permissionPairs = featureNames.map((feature: LicensePermission) => { + const { entitled, limit, actual } = features[feature] + const limitCompliant = limit && actual && limit >= actual + return [feature, entitled && limitCompliant] + }) + return Object.fromEntries(permissionPairs) +} diff --git a/site/src/xServices/license/licenseXService.ts b/site/src/xServices/license/licenseXService.ts new file mode 100644 index 0000000000000..0940cf7683b1b --- /dev/null +++ b/site/src/xServices/license/licenseXService.ts @@ -0,0 +1,94 @@ +import { assign, createMachine } from "xstate" +import * as API from "../../api/api" +import { LicenseData } from "../../api/types" + +export const Language = { + getLicenseError: "Error getting license information.", +} + +/* deserves more thought but this is one way to handle unlicensed cases */ +const defaultLicenseData = { + warnings: [], + features: { + audit: { + enabled: false, + entitled: false + }, + createUser: { + enabled: true, + entitled: true, + limit: 0 + }, + createOrg: { + enabled: false, + entitled: false + } + } +} + +export type LicenseContext = { + licenseData: LicenseData + getLicenseError?: Error | unknown +} + +export type LicenseEvent = { + type: "GET_LICENSE_DATA" +} + +export const licenseMachine = createMachine( + { + id: "licenseMachine", + initial: "idle", + schema: { + context: {} as LicenseContext, + events: {} as LicenseEvent, + services: { + getLicenseData: { + data: {} as LicenseData, + }, + }, + }, + tsTypes: {} as import("./licenseXService.typegen").Typegen0, + context: { + licenseData: defaultLicenseData + }, + states: { + idle: { + on: { + GET_LICENSE_DATA: "gettingLicenseData", + }, + }, + gettingLicenseData: { + entry: "clearGetLicenseError", + invoke: { + id: "getLicenseData", + src: "getLicenseData", + onDone: { + target: "idle", + actions: ["assignLicenseData"], + }, + onError: { + target: "idle", + actions: ["assignGetLicenseError"], + }, + }, + }, + }, + }, + { + actions: { + assignLicenseData: assign({ + licenseData: (_, event) => event.data, + }), + assignGetLicenseError: assign({ + getLicenseError: (_, event) => event.data, + }), + clearGetLicenseError: assign({ + getLicenseError: (_) => undefined, + }), + }, + services: { + getLicenseData: () => API.getLicenseData(), + }, + }, +) From 9aa9f04c81efb7f76a22630a51cbe3a7834b7491 Mon Sep 17 00:00:00 2001 From: presleyp Date: Fri, 15 Jul 2022 18:01:35 +0000 Subject: [PATCH 3/7] add LicenseBanner that calls xservice --- .../LicenseBanner/LicenseBanner.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 site/src/components/LicenseBanner/LicenseBanner.tsx diff --git a/site/src/components/LicenseBanner/LicenseBanner.tsx b/site/src/components/LicenseBanner/LicenseBanner.tsx new file mode 100644 index 0000000000000..7445fea4c497b --- /dev/null +++ b/site/src/components/LicenseBanner/LicenseBanner.tsx @@ -0,0 +1,20 @@ +import { useActor } from "@xstate/react" +import { useContext, useEffect } from "react" +import { XServiceContext } from "../../xServices/StateContext" + +export const LicenseBanner: React.FC = () => { + const xServices = useContext(XServiceContext) + const [licenseState, licenseSend] = useActor(xServices.licenseXService) + const warnings = licenseState.context.licenseData.warnings + + /** Gets license data on app mount because LicenseBanner is mounted in App */ + useEffect(() => { + licenseSend("GET_LICENSE_DATA") + }, [licenseSend]) + + if (warnings) { + return
{warnings.map((warning, i) =>

{warning}

)}
+ } else { + return null + } +} From cd600e21ac0be7cb5e9abc74fa8bd3f453acc141 Mon Sep 17 00:00:00 2001 From: presleyp Date: Fri, 15 Jul 2022 18:02:08 +0000 Subject: [PATCH 4/7] Mount license banner --- site/src/app.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/src/app.tsx b/site/src/app.tsx index 8782f663884d6..b6ba3b324f150 100644 --- a/site/src/app.tsx +++ b/site/src/app.tsx @@ -6,6 +6,7 @@ import { SWRConfig } from "swr" import { AppRouter } from "./AppRouter" import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary" import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar" +import { LicenseBanner } from "./components/LicenseBanner/LicenseBanner" import { dark } from "./theme" import "./theme/globalFonts" import { XServiceProvider } from "./xServices/StateContext" @@ -35,6 +36,7 @@ export const App: FC = () => { + From 371661297e60532a9c36da2959c602fcc0565445 Mon Sep 17 00:00:00 2001 From: presleyp Date: Fri, 15 Jul 2022 18:02:54 +0000 Subject: [PATCH 5/7] Show stub audit log page if feature enabled --- site/src/AppRouter.tsx | 3 +++ .../RequireLicense/RequireLicense.tsx | 24 +++++++++++++++++++ site/src/pages/AuditLogPage/AuditLogPage.tsx | 5 ++++ 3 files changed, 32 insertions(+) create mode 100644 site/src/components/RequireLicense/RequireLicense.tsx create mode 100644 site/src/pages/AuditLogPage/AuditLogPage.tsx diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index d76dbc687da77..196522900655a 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -2,9 +2,11 @@ import { FC, lazy, Suspense } from "react" import { Route, Routes } from "react-router-dom" import { AuthAndFrame } from "./components/AuthAndFrame/AuthAndFrame" import { RequireAuth } from "./components/RequireAuth/RequireAuth" +import { RequireLicense } from "./components/RequireLicense/RequireLicense" import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout" import { IndexPage } from "./pages" import { NotFoundPage } from "./pages/404Page/404Page" +import { AuditLogPage } from "./pages/AuditLogPage/AuditLogPage" import { CliAuthenticationPage } from "./pages/CliAuthPage/CliAuthPage" import { HealthzPage } from "./pages/HealthzPage/HealthzPage" import { LoginPage } from "./pages/LoginPage/LoginPage" @@ -113,6 +115,7 @@ export const AppRouter: FC = () => ( } /> } /> } /> + } /> diff --git a/site/src/components/RequireLicense/RequireLicense.tsx b/site/src/components/RequireLicense/RequireLicense.tsx new file mode 100644 index 0000000000000..a65844c9597a4 --- /dev/null +++ b/site/src/components/RequireLicense/RequireLicense.tsx @@ -0,0 +1,24 @@ +import { useSelector } from "@xstate/react" +import React, { useContext } from "react" +import { Navigate } from "react-router" +import { FullScreenLoader } from "../Loader/FullScreenLoader" +import { XServiceContext } from "../../xServices/StateContext" +import { selectLicenseVisibility } from "../../xServices/license/licenseSelectors" +import { LicensePermission } from "../../api/types" + +export interface RequireLicenseProps { + children: JSX.Element + permissionRequired: LicensePermission +} + +export const RequireLicense: React.FC = ({ children, permissionRequired }) => { + const xServices = useContext(XServiceContext) + const visibility = useSelector(xServices.licenseXService, selectLicenseVisibility) + if (!visibility) { + return + } else if (!visibility[permissionRequired]) { + return + } else { + return children + } +} diff --git a/site/src/pages/AuditLogPage/AuditLogPage.tsx b/site/src/pages/AuditLogPage/AuditLogPage.tsx new file mode 100644 index 0000000000000..9e054ae1735f7 --- /dev/null +++ b/site/src/pages/AuditLogPage/AuditLogPage.tsx @@ -0,0 +1,5 @@ +export const AuditLogPage = () => { + return
+ This is a stub for the audit log page. +
+} From d372600549dcc8856dc11e75c2b0c858fb46deab Mon Sep 17 00:00:00 2001 From: presleyp Date: Fri, 15 Jul 2022 18:40:58 +0000 Subject: [PATCH 6/7] Move audit page and add nav link --- site/src/AppRouter.tsx | 4 +++- site/src/components/Navbar/Navbar.tsx | 7 +++++-- site/src/components/NavbarView/NavbarView.tsx | 11 ++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 196522900655a..787f4d9e964a8 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -111,11 +111,13 @@ export const AppRouter: FC = () => ( />
+ + } /> + }> } /> } /> } /> - } /> diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index 0ac64ef7d1269..9a74375b09804 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -1,5 +1,6 @@ -import { useActor } from "@xstate/react" +import { useActor, useSelector } from "@xstate/react" import React, { useContext } from "react" +import { selectLicenseVisibility } from "../../xServices/license/licenseSelectors" import { XServiceContext } from "../../xServices/StateContext" import { NavbarView } from "../NavbarView/NavbarView" @@ -9,5 +10,7 @@ export const Navbar: React.FC = () => { const { me } = authState.context const onSignOut = () => authSend("SIGN_OUT") - return + const showAuditLog = useSelector(xServices.licenseXService, selectLicenseVisibility)["audit"] + + return } diff --git a/site/src/components/NavbarView/NavbarView.tsx b/site/src/components/NavbarView/NavbarView.tsx index 26d9fa2d0fb2a..bba6925437393 100644 --- a/site/src/components/NavbarView/NavbarView.tsx +++ b/site/src/components/NavbarView/NavbarView.tsx @@ -11,15 +11,17 @@ import { UserDropdown } from "../UserDropdown/UsersDropdown" export interface NavbarViewProps { user?: TypesGen.User onSignOut: () => void + showAuditLog: boolean } export const Language = { workspaces: "Workspaces", templates: "Templates", users: "Users", + audit: "Audit" } -export const NavbarView: React.FC = ({ user, onSignOut }) => { +export const NavbarView: React.FC = ({ user, onSignOut, showAuditLog }) => { const styles = useStyles() const location = useLocation() return ( @@ -51,6 +53,13 @@ export const NavbarView: React.FC = ({ user, onSignOut }) => { {Language.users} + {showAuditLog && + + + {Language.audit} + + + }
From 0ae85e3dd5bffd3db2bb35611d9c40dbf2e2b91e Mon Sep 17 00:00:00 2001 From: presleyp Date: Mon, 18 Jul 2022 20:52:12 +0000 Subject: [PATCH 7/7] Example smaller feature --- site/src/api/api.ts | 4 ++++ site/src/api/types.ts | 2 +- site/src/components/Workspace/Workspace.tsx | 2 ++ .../components/WorkspaceSchedule/WorkspaceSchedule.tsx | 9 +++++++-- .../WorkspaceScheduleButton/CELAdminScheduleLabel.tsx | 9 +++++++++ .../WorkspaceScheduleButton/CELChangeScheduleLink.tsx | 10 ++++++++++ .../WorkspaceScheduleButton/OSSChangeScheduleLink.tsx | 9 +++++++++ .../WorkspaceScheduleButton.tsx | 6 +++++- site/src/pages/WorkspacePage/WorkspacePage.tsx | 3 +++ site/src/xServices/license/licenseXService.ts | 4 ++++ 10 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 site/src/components/WorkspaceScheduleButton/CELAdminScheduleLabel.tsx create mode 100644 site/src/components/WorkspaceScheduleButton/CELChangeScheduleLink.tsx create mode 100644 site/src/components/WorkspaceScheduleButton/OSSChangeScheduleLink.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b2a4ee8901f5a..66814e0b5726e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -84,6 +84,10 @@ export const getLicenseData = async (): Promise => { createOrg: { entitled: true, enabled: false + }, + adminScheduling: { + enabled: true, + entitled: true } }, warnings: ["This is a test license compliance banner", "Here is a second one"] diff --git a/site/src/api/types.ts b/site/src/api/types.ts index 340d1c078c092..6f70b1a40b43b 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -15,7 +15,7 @@ export type WorkspaceBuildTransition = "start" | "stop" | "delete" export type Message = { message: string } -export type LicensePermission = "audit" | "createUser" | "createOrg" +export type LicensePermission = "audit" | "createUser" | "createOrg" | "adminScheduling" export type LicenseFeatures = Record void } scheduleProps: { + adminScheduling: boolean onDeadlinePlus: () => void onDeadlineMinus: () => void } @@ -61,6 +62,7 @@ export const Workspace: FC = ({ actions={ = ({ workspace, canUpdateWorkspace, + adminScheduling }) => { const styles = useStyles() const timezone = workspace.autostart_schedule @@ -71,7 +76,7 @@ export const WorkspaceSchedule: FC = ({ component={RouterLink} to={`/@${workspace.owner_name}/${workspace.name}/schedule`} > - {Language.editScheduleLink} + {adminScheduling ? : }
)} diff --git a/site/src/components/WorkspaceScheduleButton/CELAdminScheduleLabel.tsx b/site/src/components/WorkspaceScheduleButton/CELAdminScheduleLabel.tsx new file mode 100644 index 0000000000000..a40234e830bb0 --- /dev/null +++ b/site/src/components/WorkspaceScheduleButton/CELAdminScheduleLabel.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const Language = { + orgDefault: "Org default:" +} + +export const CELAdminScheduleLabel: React.FC = () => { + return {Language.orgDefault}  +} diff --git a/site/src/components/WorkspaceScheduleButton/CELChangeScheduleLink.tsx b/site/src/components/WorkspaceScheduleButton/CELChangeScheduleLink.tsx new file mode 100644 index 0000000000000..202aeefd5c8f9 --- /dev/null +++ b/site/src/components/WorkspaceScheduleButton/CELChangeScheduleLink.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +const Language = { + overrideScheduleLink: "Override Schedule" +} + +export const CELChangeScheduleLink: React.FC = () => { + return {Language.overrideScheduleLink} +} + diff --git a/site/src/components/WorkspaceScheduleButton/OSSChangeScheduleLink.tsx b/site/src/components/WorkspaceScheduleButton/OSSChangeScheduleLink.tsx new file mode 100644 index 0000000000000..9d88f8bc43ada --- /dev/null +++ b/site/src/components/WorkspaceScheduleButton/OSSChangeScheduleLink.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const Language = { + editScheduleLink: "Edit Schedule" +} + +export const OSSChangeScheduleLink: React.FC = () => { + return {Language.editScheduleLink} +} diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx index d03c0ad357136..13bdfcae4a727 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx @@ -17,6 +17,7 @@ import { Workspace } from "../../api/typesGenerated" import { isWorkspaceOn } from "../../util/workspace" import { Stack } from "../Stack/Stack" import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule" +import { CELAdminScheduleLabel } from "./CELAdminScheduleLabel" import { WorkspaceScheduleLabel } from "./WorkspaceScheduleLabel" // REMARK: some plugins depend on utc, so it's listed first. Otherwise they're @@ -55,6 +56,7 @@ export interface WorkspaceScheduleButtonProps { onDeadlinePlus: () => void onDeadlineMinus: () => void canUpdateWorkspace: boolean + adminScheduling: boolean } export const WorkspaceScheduleButton: React.FC = ({ @@ -62,6 +64,7 @@ export const WorkspaceScheduleButton: React.FC = ( onDeadlinePlus, onDeadlineMinus, canUpdateWorkspace, + adminScheduling }) => { const anchorRef = useRef(null) const [isOpen, setIsOpen] = useState(false) @@ -75,6 +78,7 @@ export const WorkspaceScheduleButton: React.FC = ( return (
+ {adminScheduling && } {canUpdateWorkspace && shouldDisplayPlusMinus(workspace) && ( @@ -126,7 +130,7 @@ export const WorkspaceScheduleButton: React.FC = ( horizontal: "right", }} > - +
diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 7dfbac41e803c..787ce67f91d6e 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -11,6 +11,7 @@ import { Workspace } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" import { selectUser } from "../../xServices/auth/authSelectors" +import { selectLicenseVisibility } from "../../xServices/license/licenseSelectors" import { XServiceContext } from "../../xServices/StateContext" import { workspaceMachine } from "../../xServices/workspace/workspaceXService" import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService" @@ -24,6 +25,7 @@ export const WorkspacePage: React.FC = () => { const xServices = useContext(XServiceContext) const me = useSelector(xServices.authXService, selectUser) + const adminScheduling = useSelector(xServices.licenseXService, selectLicenseVisibility)["adminScheduling"] const [workspaceState, workspaceSend] = useMachine(workspaceMachine, { context: { @@ -68,6 +70,7 @@ export const WorkspacePage: React.FC = () => { }, }} scheduleProps={{ + adminScheduling, onDeadlineMinus: () => { bannerSend({ type: "UPDATE_DEADLINE", diff --git a/site/src/xServices/license/licenseXService.ts b/site/src/xServices/license/licenseXService.ts index 0940cf7683b1b..6b1b9513e8da7 100644 --- a/site/src/xServices/license/licenseXService.ts +++ b/site/src/xServices/license/licenseXService.ts @@ -22,6 +22,10 @@ const defaultLicenseData = { createOrg: { enabled: false, entitled: false + }, + adminScheduling: { + enabled: true, + entitled: true } } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy