Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed
presleyp wants to merge7 commits intomainfromlicensing-poc/presleyp/2904
Closed
Show file tree
Hide file tree
Changes fromall 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 deletionssite/src/AppRouter.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand 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 deletionssite/src/api/api.ts
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionssite/src/api/types.ts
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionssite/src/app.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand Up@@ -35,6 +36,7 @@ export const App: FC = () => {
<CssBaseline />
<ErrorBoundary>
<XServiceProvider>
<LicenseBanner />
<AppRouter />
<GlobalSnackbar />
</XServiceProvider>
Expand Down
20 changes: 20 additions & 0 deletionssite/src/components/LicenseBanner/LicenseBanner.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionssite/src/components/Navbar/Navbar.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionsite/src/components/NavbarView/NavbarView.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand 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 deletionssite/src/components/RequireLicense/RequireLicense.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionssite/src/components/Workspace/Workspace.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,7 @@ export interface WorkspaceProps {
onExtend: () => void
}
scheduleProps: {
adminScheduling: boolean
onDeadlinePlus: () => void
onDeadlineMinus: () => void
}
Expand DownExpand 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
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand 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
View file
Open in desktop
Original file line numberDiff line numberDiff 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>
}
View file
Open in desktop
Original file line numberDiff line numberDiff 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>
}

View file
Open in desktop
Original file line numberDiff line numberDiff 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>
}
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 deletionssite/src/pages/AuditLogPage/AuditLogPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionssite/src/pages/WorkspacePage/WorkspacePage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand Up@@ -68,6 +70,7 @@ export const WorkspacePage: React.FC = () => {
},
}}
scheduleProps={{
adminScheduling,
onDeadlineMinus: () => {
bannerSend({
type: "UPDATE_DEADLINE",
Expand Down
3 changes: 3 additions & 0 deletionssite/src/xServices/StateContext.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand 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 deletionssite/src/xServices/license/licenseSelectors.ts
View file
Open in desktop
Original file line numberDiff line numberDiff 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

[8]ページ先頭

©2009-2025 Movatter.jp