Skip to content

[wip][dashboard] license tab in the admin dashboard #9031

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 2 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
9 changes: 8 additions & 1 deletion components/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import { UserContext } from "./user-context";
import { TeamsContext } from "./teams/teams-context";
import { ThemeContext } from "./theme-context";
import { AdminContext } from "./admin-context";
import { LicenseContext } from "./license-context";
import { getGitpodService } from "./service/service";
import { shouldSeeWhatsNew, WhatsNew } from "./whatsnew/WhatsNew";
import gitpodIcon from "./icons/gitpod.svg";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { useHistory } from "react-router-dom";
import { trackButtonOrAnchor, trackPathChange, trackLocation } from "./Analytics";
import { User } from "@gitpod/gitpod-protocol";
import { LicenseInfo, User } from "@gitpod/gitpod-protocol";
import * as GitpodCookie from "@gitpod/gitpod-protocol/lib/util/gitpod-cookie";
import { Experiment } from "./experiments";
import { workspacesPathMain } from "./workspaces/workspaces.routes";
Expand Down Expand Up @@ -77,6 +78,7 @@ const AdminSettings = React.lazy(() => import(/* webpackPrefetch: true */ "./adm
const ProjectsSearch = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/ProjectsSearch"));
const TeamsSearch = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/TeamsSearch"));
const OAuthClientApproval = React.lazy(() => import(/* webpackPrefetch: true */ "./OauthClientApproval"));
const License = React.lazy(() => import(/* webpackPrefetch: true */ "./admin/License"));

function Loading() {
return <></>;
Expand Down Expand Up @@ -150,6 +152,7 @@ function App() {
const { teams, setTeams } = useContext(TeamsContext);
const { setAdminSettings } = useContext(AdminContext);
const { setIsDark } = useContext(ThemeContext);
const { setLicense } = useContext(LicenseContext);

const [loading, setLoading] = useState<boolean>(true);
const [isWhatsNewShown, setWhatsNewShown] = useState(false);
Expand Down Expand Up @@ -186,6 +189,9 @@ function App() {
if (user?.rolesOrPermissions?.includes("admin")) {
const adminSettings = await getGitpodService().server.adminGetSettings();
setAdminSettings(adminSettings);

var license: LicenseInfo = await getGitpodService().server.adminGetLicense();
setLicense(license);
}
} catch (error) {
console.error(error);
Expand Down Expand Up @@ -367,6 +373,7 @@ function App() {
<AdminRoute path="/admin/teams" component={TeamsSearch} />
<AdminRoute path="/admin/workspaces" component={WorkspacesSearch} />
<AdminRoute path="/admin/projects" component={ProjectsSearch} />
<AdminRoute path="/admin/license" component={License} />
<AdminRoute path="/admin/settings" component={AdminSettings} />

<Route path={["/", "/login"]} exact>
Expand Down
177 changes: 177 additions & 0 deletions components/dashboard/src/admin/License.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { PageWithSubMenu } from "../components/PageWithSubMenu";
import { adminMenu } from "./admin-menu";

import { LicenseContext } from "../license-context";
import { ReactElement, useContext, useEffect } from "react";
import { getGitpodService } from "../service/service";

import { ReactComponent as Alert } from "../images/exclamation.svg";
import { ReactComponent as Success } from "../images/tick.svg";
import { ReactComponent as Tick } from "../images/check.svg";
import { ReactComponent as Cross } from "../images/x.svg";

export default function License() {
const { license, setLicense } = useContext(LicenseContext);

useEffect(() => {
if (isGitpodIo()) {
return; // temporarily disable to avoid hight CPU on the DB
}
(async () => {
const data = await getGitpodService().server.adminGetLicense();
setLicense(data);
})();
}, []);

const featureList = license?.enabledFeatures;
const features = license?.features;

// if user seats is 0, it means that there is user limit in the license
const userLimit = license?.seats == 0 ? "Unlimited" : license?.seats;

const [licenseLevel, paid, msg, tick] = getSubscriptionLevel(
license?.plan || "",
license?.userCount || 0,
license?.seats || 0,
license?.fallbackAllowed || false,
);

return (
<div>
<PageWithSubMenu
subMenu={adminMenu}
title="License"
subtitle="License associated with your Gitpod Installation"
>
<div className="flex flex-row space-x-4">
<Card className="bg-gray-800 dark:bg-white text-white dark:text-gray-400">
<p className="text-white dark:text-black font-bold pt-4 text-sm"> {licenseLevel}</p>
<p className="dark:text-gray-500">{paid}</p>
<p className="text-gray-400 pt-4 pb-2">Available features:</p>
{features &&
features.map((feat: string) => (
<div className="flex">
{featureList?.includes(feat) ? (
<Tick className="h-4" />
) : (
<Cross className="h-4 w-4 px-1" />
)}
{capitalizeInitials(feat)}
</div>
))}
</Card>
<Card className="bg-gray-200 dark:bg-gray-900 text-gray-600 dark:text-gray-600">
<div className="text-gray-600 dark:text-gray-200 text-sm py-4 flex-row flex items-center">
<div>{msg}</div>
<div className="px-4">{getLicenseValidIcon(tick)}</div>
</div>
<p className="dark:text-gray-500">Registered Users</p>
<span className="dark:text-gray-300 pt-1 text-lg">{license?.userCount || 0}</span>
<span className="dark:text-gray-500 text-gray-400 pt-1 text-lg"> / {userLimit} </span>
<p className="dark:text-gray-500 pt-2 ">License Type</p>
<h4 className="dark:text-gray-300 pt-1 text-lg">{capitalizeInitials(license?.type || "")}</h4>
<button
type="button"
onClick={(e) => {
e.preventDefault();
window.location.href = "https://www.gitpod.io/self-hosted";
}}
className="ml-2 float-right"
>
{license?.plan == "prod" ? "Contact Sales" : "Request License"}
</button>
</Card>
</div>
</PageWithSubMenu>
</div>
);
}

function capitalizeInitials(str: string): string {
return str
.split("-")
.map((item) => {
return item.charAt(0).toUpperCase() + item.slice(1);
})
.join(" ");
}

function getSubscriptionLevel(level: string, userCount: number, seats: number, fallbackAllowed: boolean): string[] {
switch (level) {
case "prod": {
return professionalPlan(userCount, seats);
}
case "community": {
return communityPlan(userCount, seats, fallbackAllowed);
}
case "trial": {
return ["Trial", "Free", "You have a trial license.", "grey-tick"];
}
default: {
return ["Unknown", "Free", "No active licenses.", "red-cross"];
}
}
}

function professionalPlan(userCount: number, seats: number): string[] {
const aboveLimit: boolean = userCount > seats;
let msg: string, tick: string;
if (aboveLimit) {
msg = "You have exceeded the usage limit.";
tick = "red-cross";
} else {
msg = "You have an active professional license.";
tick = "green-tick";
}

return ["Professional", "Paid", msg, tick];
}

function communityPlan(userCount: number, seats: number, fallbackAllowed: boolean): string[] {
const aboveLimit: boolean = userCount > seats;

let msg: string = "You are using the free community edition";
let tick: string = "green-tick";
if (aboveLimit) {
if (fallbackAllowed) {
msg = "No active license. You are using the community edition.";
tick = "grey-tick";
} else {
msg = "No active license. You have exceeded the usage limit.";
tick = "red-cross";
}
}

return ["Community", "Free", msg, tick];
}

function getLicenseValidIcon(iconname: string): ReactElement {
switch (iconname) {
case "green-tick":
return <Success fill="green" className="h-8 w-8" />;
case "grey-tick":
return <Success fill="gray" className="h-8 w-8" />;
case "red-cross":
return <Alert fill="red" className="h-8 w-8" />;
default:
return <Alert fill="gray" className="h-8 w-8" />;
}
}

function isGitpodIo() {
return window.location.hostname === "gitpod.io" || window.location.hostname === "gitpod-staging.com";
}

function Card(p: { className?: string; children?: React.ReactNode }) {
return (
<div className={"flex rounded-xl font-semibold text-xs w-72 h-64 px-4 " + (p.className || "")}>
<span>{p.children}</span>
</div>
);
}
4 changes: 4 additions & 0 deletions components/dashboard/src/admin/admin-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const adminMenu = [
title: "Teams",
link: ["/admin/teams"],
},
{
title: "License",
link: ["/admin/license"],
},
{
title: "Settings",
link: ["/admin/settings"],
Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/images/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions components/dashboard/src/images/tick.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 14 additions & 11 deletions components/dashboard/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import App from "./App";
import { UserContextProvider } from "./user-context";
import { AdminContextProvider } from "./admin-context";
import { PaymentContextProvider } from "./payment-context";
import { LicenseContextProvider } from "./license-context";
import { TeamsContextProvider } from "./teams/teams-context";
import { ProjectContextProvider } from "./projects/project-context";
import { ThemeContextProvider } from "./theme-context";
Expand All @@ -23,17 +24,19 @@ ReactDOM.render(
<UserContextProvider>
<AdminContextProvider>
<PaymentContextProvider>
<TeamsContextProvider>
<ProjectContextProvider>
<ThemeContextProvider>
<StartWorkspaceModalContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</StartWorkspaceModalContextProvider>
</ThemeContextProvider>
</ProjectContextProvider>
</TeamsContextProvider>
<LicenseContextProvider>
<TeamsContextProvider>
<ProjectContextProvider>
<ThemeContextProvider>
<StartWorkspaceModalContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</StartWorkspaceModalContextProvider>
</ThemeContextProvider>
</ProjectContextProvider>
</TeamsContextProvider>
</LicenseContextProvider>
</PaymentContextProvider>
</AdminContextProvider>
</UserContextProvider>
Expand Down
22 changes: 22 additions & 0 deletions components/dashboard/src/license-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import React, { createContext, useState } from "react";
import { LicenseInfo } from "@gitpod/gitpod-protocol";

const LicenseContext = createContext<{
license?: LicenseInfo;
setLicense: React.Dispatch<LicenseInfo>;
}>({
setLicense: () => null,
});

const LicenseContextProvider: React.FC = ({ children }) => {
const [license, setLicense] = useState<LicenseInfo>();
return <LicenseContext.Provider value={{ license, setLicense }}>{children}</LicenseContext.Provider>;
};

export { LicenseContext, LicenseContextProvider };
7 changes: 7 additions & 0 deletions components/gitpod-protocol/src/license-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ export type LicenseIssue = "seats-exhausted";
export interface LicenseInfo {
key: string;
seats: number;
userCount?: number;
valid: boolean;
validUntil: string;
plan?: string;
features?: string[];
enabledFeatures?: string[];
type?: string;
errorMsg?: string;
fallbackAllowed: boolean;
}

export interface GetLicenseInfoResult {
Expand All @@ -33,5 +39,6 @@ export enum LicenseFeature {
export interface LicenseService {
validateLicense(): Promise<LicenseValidationResult>;
getLicenseInfo(): Promise<GetLicenseInfoResult>;
adminGetLicense(): Promise<LicenseInfo>;
licenseIncludesFeature(feature: LicenseFeature): Promise<boolean>;
}
4 changes: 3 additions & 1 deletion components/licensor/ee/pkg/licensor/gitpod.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func NewGitpodEvaluator(key []byte, domain string) (res *Evaluator) {
return &Evaluator{
lic: defaultLicense,
allowFallback: true,
plan: CommunityLicense,
}
}

Expand Down Expand Up @@ -63,6 +64,7 @@ func NewGitpodEvaluator(key []byte, domain string) (res *Evaluator) {

return &Evaluator{
lic: lic.LicensePayload,
allowFallback: false, // Gitpod licenses cannot fallback - assume these are always paid-for
allowFallback: false, // Gitpod licenses cannot fallback - assume these are always paid-for
plan: ProfessionalLicense, // This essentially means "paid" license
}
}
Loading