Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Proof of concept for licensing #3008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -109,6 +111,9 @@ export const AppRouter: FC = () => (
/>
</Route>


<Route path="audit" element={<RequireLicense permissionRequired="audit"><AuditLogPage /></RequireLicense>} />

<Route path="settings" element={<SettingsLayout />}>
<Route path="account" element={<AccountPage />} />
<Route path="security" element={<SecurityPage />} />
Expand Down
27 changes: 27 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,33 @@ export const checkUserPermissions = async (
return response.data
}

export const getLicenseData = async (): Promise<Types.LicenseData> => {
const fakeLicenseData = {
features: {
audit: {
entitled: false,
enabled: true
},
createUser: {
entitled: true,
enabled: true,
limit: 1,
actual: 2
},
createOrg: {
entitled: true,
enabled: false
},
adminScheduling: {
enabled: true,
entitled: true
}
},
warnings: ["This is a test license compliance banner", "Here is a second one"]
}
return Promise.resolve(fakeLicenseData)
}

export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
const response = await axios.post<TypesGen.GenerateAPIKeyResponse>("/api/v2/users/me/keys")
return response.data
Expand Down
14 changes: 14 additions & 0 deletions site/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ export interface ReconnectingPTYRequest {
export type WorkspaceBuildTransition = "start" | "stop" | "delete"

export type Message = { message: string }

export type LicensePermission = "audit" | "createUser" | "createOrg" | "adminScheduling"

export type LicenseFeatures = Record<LicensePermission, {
entitled: boolean
enabled: boolean
limit?: number
actual?: number
}>

export type LicenseData = {
features: LicenseFeatures
warnings: string[]
}
2 changes: 2 additions & 0 deletions site/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -35,6 +36,7 @@ export const App: FC = () => {
<CssBaseline />
<ErrorBoundary>
<XServiceProvider>
<LicenseBanner />
<AppRouter />
<GlobalSnackbar />
</XServiceProvider>
Expand Down
20 changes: 20 additions & 0 deletions site/src/components/LicenseBanner/LicenseBanner.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>{warnings.map((warning, i) => <p key={`${i}`}>{warning}</p>)}</div>
} else {
return null
}
}
7 changes: 5 additions & 2 deletions site/src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -9,5 +10,7 @@ export const Navbar: React.FC = () => {
const { me } = authState.context
const onSignOut = () => authSend("SIGN_OUT")

return <NavbarView user={me} onSignOut={onSignOut} />
const showAuditLog = useSelector(xServices.licenseXService, selectLicenseVisibility)["audit"]

return <NavbarView user={me} onSignOut={onSignOut} showAuditLog={showAuditLog} />
}
11 changes: 10 additions & 1 deletion site/src/components/NavbarView/NavbarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<NavbarViewProps> = ({ user, onSignOut }) => {
export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut, showAuditLog }) => {
const styles = useStyles()
const location = useLocation()
return (
Expand Down Expand Up @@ -51,6 +53,13 @@ export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut }) => {
{Language.users}
</NavLink>
</ListItem>
{showAuditLog &&
<ListItem button className={styles.item}>
<NavLink className={styles.link} to="/audit">
{Language.audit}
</NavLink>
</ListItem>
}
</List>
<div className={styles.fullWidth} />
<div className={styles.fixed}>
Expand Down
24 changes: 24 additions & 0 deletions site/src/components/RequireLicense/RequireLicense.tsx
Original file line number Diff line number Diff line change
@@ -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<RequireLicenseProps> = ({ children, permissionRequired }) => {
const xServices = useContext(XServiceContext)
const visibility = useSelector(xServices.licenseXService, selectLicenseVisibility)
if (!visibility) {
return <FullScreenLoader />
} else if (!visibility[permissionRequired]) {
return <Navigate to="/not-found" />
} else {
return children
}
}
2 changes: 2 additions & 0 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface WorkspaceProps {
onExtend: () => void
}
scheduleProps: {
adminScheduling: boolean
onDeadlinePlus: () => void
onDeadlineMinus: () => void
}
Expand Down Expand Up @@ -61,6 +62,7 @@ export const Workspace: FC<WorkspaceProps> = ({
actions={
<Stack direction="row" spacing={1}>
<WorkspaceScheduleButton
adminScheduling={scheduleProps.adminScheduling}
workspace={workspace}
onDeadlineMinus={scheduleProps.onDeadlineMinus}
onDeadlinePlus={scheduleProps.onDeadlinePlus}
Expand Down
9 changes: 7 additions & 2 deletions site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
Language as ScheduleLanguage,
} from "../../util/schedule"
import { Stack } from "../Stack/Stack"
import { CELChangeScheduleLink } from "../WorkspaceScheduleButton/CELChangeScheduleLink"
import { OSSChangeScheduleLink } from "../WorkspaceScheduleButton/OSSChangeScheduleLink"

// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
// sorted alphabetically.
Expand All @@ -27,18 +29,21 @@ dayjs.extend(relativeTime)
dayjs.extend(timezone)

export const Language = {
editScheduleLink: "Edit schedule",
OSSeditScheduleLink: "Edit schedule",
CELoverrideScheduleLink: "Override schedule",
timezoneLabel: "Timezone",
}

export interface WorkspaceScheduleProps {
workspace: Workspace
canUpdateWorkspace: boolean
adminScheduling: boolean
}

export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({
workspace,
canUpdateWorkspace,
adminScheduling
}) => {
const styles = useStyles()
const timezone = workspace.autostart_schedule
Expand Down Expand Up @@ -71,7 +76,7 @@ export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({
component={RouterLink}
to={`/@${workspace.owner_name}/${workspace.name}/schedule`}
>
{Language.editScheduleLink}
{adminScheduling ? <CELChangeScheduleLink /> : <OSSChangeScheduleLink />}
</Link>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

const Language = {
orgDefault: "Org default:"
}

export const CELAdminScheduleLabel: React.FC = () => {
return <span>{Language.orgDefault}&nbsp;</span>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'

const Language = {
overrideScheduleLink: "Override Schedule"
}

export const CELChangeScheduleLink: React.FC = () => {
return <span>{Language.overrideScheduleLink}</span>
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

const Language = {
editScheduleLink: "Edit Schedule"
}

export const OSSChangeScheduleLink: React.FC = () => {
return <span>{Language.editScheduleLink}</span>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,13 +56,15 @@ export interface WorkspaceScheduleButtonProps {
onDeadlinePlus: () => void
onDeadlineMinus: () => void
canUpdateWorkspace: boolean
adminScheduling: boolean
}

export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = ({
workspace,
onDeadlinePlus,
onDeadlineMinus,
canUpdateWorkspace,
adminScheduling
}) => {
const anchorRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
Expand All @@ -75,6 +78,7 @@ export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = (
return (
<div className={styles.wrapper}>
<div className={styles.label}>
{adminScheduling && <CELAdminScheduleLabel />}
<WorkspaceScheduleLabel workspace={workspace} />
{canUpdateWorkspace && shouldDisplayPlusMinus(workspace) && (
<Stack direction="row" spacing={0}>
Expand Down Expand Up @@ -126,7 +130,7 @@ export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = (
horizontal: "right",
}}
>
<WorkspaceSchedule workspace={workspace} canUpdateWorkspace={canUpdateWorkspace} />
<WorkspaceSchedule workspace={workspace} canUpdateWorkspace={canUpdateWorkspace} adminScheduling={adminScheduling} />
</Popover>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions site/src/pages/AuditLogPage/AuditLogPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const AuditLogPage = () => {
return <div>
This is a stub for the audit log page.
</div>
}
3 changes: 3 additions & 0 deletions site/src/pages/WorkspacePage/WorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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: {
Expand Down Expand Up @@ -68,6 +70,7 @@ export const WorkspacePage: React.FC = () => {
},
}}
scheduleProps={{
adminScheduling,
onDeadlineMinus: () => {
bannerSend({
type: "UPDATE_DEADLINE",
Expand Down
3 changes: 3 additions & 0 deletions site/src/xServices/StateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -12,6 +13,7 @@ interface XServiceContextType {
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
usersXService: ActorRefFrom<typeof usersMachine>
siteRolesXService: ActorRefFrom<typeof siteRolesMachine>
licenseXService: ActorRefFrom<typeof licenseMachine>
}

/**
Expand Down Expand Up @@ -39,6 +41,7 @@ export const XServiceProvider: React.FC = ({ children }) => {
usersMachine.withConfig({ actions: { redirectToUsersPage } }),
),
siteRolesXService: useInterpret(siteRolesMachine),
licenseXService: useInterpret(licenseMachine)
}}
>
{children}
Expand Down
24 changes: 24 additions & 0 deletions site/src/xServices/license/licenseSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { State } from "xstate"
import { LicensePermission } from "../../api/types"
import { LicenseContext, LicenseEvent } from "./licenseXService"
type LicenseState = State<LicenseContext, LicenseEvent>

export const selectLicenseVisibility = (state: LicenseState): Record<LicensePermission, boolean> => {
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<LicensePermission, boolean> => {
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)
}
Loading