Skip to content

Commit

Permalink
[dashboard]: create a settings tab inside admin
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Emms authored and roboquat committed Jan 26, 2022
1 parent 27fb8a2 commit f9ec0f8
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 49 deletions.
15 changes: 12 additions & 3 deletions components/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Login } from './Login';
import { UserContext } from './user-context';
import { TeamsContext } from './teams/teams-context';
import { ThemeContext } from './theme-context';
import { AdminContext } from './admin-context';
import { getGitpodService } from './service/service';
import { shouldSeeWhatsNew, WhatsNew } from './whatsnew/WhatsNew';
import gitpodIcon from './icons/gitpod.svg';
Expand Down Expand Up @@ -52,6 +53,7 @@ const InstallGitHubApp = React.lazy(() => import(/* webpackPrefetch: true */ './
const FromReferrer = React.lazy(() => import(/* webpackPrefetch: true */ './FromReferrer'));
const UserSearch = React.lazy(() => import(/* webpackPrefetch: true */ './admin/UserSearch'));
const WorkspacesSearch = React.lazy(() => import(/* webpackPrefetch: true */ './admin/WorkspacesSearch'));
const AdminSettings = React.lazy(() => import(/* webpackPrefetch: true */ './admin/Settings'));
const OAuthClientApproval = React.lazy(() => import(/* webpackPrefetch: true */ './OauthClientApproval'));

function Loading() {
Expand Down Expand Up @@ -98,11 +100,12 @@ export function getURLHash() {
function App() {
const { user, setUser } = useContext(UserContext);
const { teams, setTeams } = useContext(TeamsContext);
const { setAdminSettings } = useContext(AdminContext);
const { setIsDark } = useContext(ThemeContext);

const [ loading, setLoading ] = useState<boolean>(true);
const [ isWhatsNewShown, setWhatsNewShown ] = useState(false);
const [ isSetupRequired, setSetupRequired ] = useState(false);
const [loading, setLoading] = useState<boolean>(true);
const [isWhatsNewShown, setWhatsNewShown] = useState(false);
const [isSetupRequired, setSetupRequired] = useState(false);
const history = useHistory();

useEffect(() => {
Expand Down Expand Up @@ -132,6 +135,11 @@ function App() {
}
}
setTeams(teams);

if (user?.rolesOrPermissions?.includes('admin')) {
const adminSettings = await getGitpodService().server.adminGetSettings();
setAdminSettings(adminSettings);
}
} catch (error) {
console.error(error);
if (error && "code" in error) {
Expand Down Expand Up @@ -279,6 +287,7 @@ function App() {

<Route path="/admin/users" component={UserSearch} />
<Route path="/admin/workspaces" component={WorkspacesSearch} />
<Route path="/admin/settings" component={AdminSettings} />

<Route path={["/", "/login"]} exact>
<Redirect to={workspacesPathMain} />
Expand Down
26 changes: 26 additions & 0 deletions components/dashboard/src/admin-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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 { InstallationAdminSettings } from "@gitpod/gitpod-protocol";

const AdminContext = createContext<{
adminSettings?: InstallationAdminSettings,
setAdminSettings: React.Dispatch<InstallationAdminSettings>,
}>({
setAdminSettings: () => null,
});

const AdminContextProvider: React.FC = ({ children }) => {
const [adminSettings, setAdminSettings] = useState<InstallationAdminSettings>();
return (
<AdminContext.Provider value={{ adminSettings, setAdminSettings }}>
{children}
</AdminContext.Provider>
);
};

export { AdminContext, AdminContextProvider };
37 changes: 37 additions & 0 deletions components/dashboard/src/admin/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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 { useContext } from "react";
import { InstallationAdminSettings } from "@gitpod/gitpod-protocol";
import { AdminContext } from "../admin-context";
import CheckBox from "../components/CheckBox";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import { getGitpodService } from "../service/service";
import { adminMenu } from "./admin-menu";

export default function Settings() {
const { adminSettings, setAdminSettings } = useContext(AdminContext);

const actuallySetTelemetryPrefs = async (value: InstallationAdminSettings) => {
await getGitpodService().server.adminUpdateSettings(value);
setAdminSettings(value);
}

return (
<div>
<PageWithSubMenu subMenu={adminMenu} title="Settings" subtitle="Configure settings for your Gitpod cluster.">
<h3>Usage Statistics</h3>
<CheckBox
title="Enable Service Ping"
desc={<span>This is used to provide insights on how you use your cluster so we can provide a better overall experience. <a className="gp-link" href="https://www.gitpod.io/privacy">Read our Privacy Policy</a></span>}
checked={adminSettings?.sendTelemetry ?? false}
onChange={(evt) => actuallySetTelemetryPrefs({
sendTelemetry: evt.target.checked,
})} />
</PageWithSubMenu>
</div >
)
}
3 changes: 3 additions & 0 deletions components/dashboard/src/admin/admin-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export const adminMenu = [{
}, {
title: 'Workspaces',
link: ['/admin/workspaces']
}, {
title: 'Settings',
link: ['/admin/settings']
},];
21 changes: 12 additions & 9 deletions components/dashboard/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { UserContextProvider } from './user-context';
import { AdminContextProvider } from './admin-context';
import { TeamsContextProvider } from './teams/teams-context';
import { ProjectContextProvider } from './projects/project-context';
import { ThemeContextProvider } from './theme-context';
Expand All @@ -18,15 +19,17 @@ import "./index.css"
ReactDOM.render(
<React.StrictMode>
<UserContextProvider>
<TeamsContextProvider>
<ProjectContextProvider>
<ThemeContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeContextProvider>
</ProjectContextProvider>
</TeamsContextProvider>
<AdminContextProvider>
<TeamsContextProvider>
<ProjectContextProvider>
<ThemeContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeContextProvider>
</ProjectContextProvider>
</TeamsContextProvider>
</AdminContextProvider>
</UserContextProvider>
</React.StrictMode>,
document.getElementById('root')
Expand Down
5 changes: 3 additions & 2 deletions components/gitpod-db/src/installation-admin-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
* See License-AGPL.txt in the project root for license information.
*/

import { InstallationAdmin } from "@gitpod/gitpod-protocol";
import { InstallationAdmin, InstallationAdminSettings } from "@gitpod/gitpod-protocol";

export const InstallationAdminDB = Symbol('InstallationAdminDB');
export interface InstallationAdminDB {
getTelemetryData(): Promise<InstallationAdmin>;
getData(): Promise<InstallationAdmin>;
setSettings(settings: Partial<InstallationAdminSettings>): Promise<void>;
}
24 changes: 14 additions & 10 deletions components/gitpod-db/src/typeorm/installation-admin-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
*/

import { inject, injectable, } from 'inversify';
import { InstallationAdmin } from '@gitpod/gitpod-protocol';
import { v4 as uuidv4 } from 'uuid';
import { InstallationAdmin, InstallationAdminSettings } from '@gitpod/gitpod-protocol';
import { Repository } from 'typeorm';
import { TypeORM } from './typeorm';
import { InstallationAdminDB } from '../installation-admin-db';
Expand All @@ -21,12 +20,7 @@ export class TypeORMInstallationAdminImpl implements InstallationAdminDB {
}

protected async createDefaultRecord(): Promise<InstallationAdmin> {
const record: InstallationAdmin = {
id: uuidv4(),
settings: {
sendTelemetry: false,
},
};
const record = InstallationAdmin.createDefault();

const repo = await this.getInstallationAdminRepo();
return repo.save(record);
Expand All @@ -37,14 +31,14 @@ export class TypeORMInstallationAdminImpl implements InstallationAdminDB {
}

/**
* Get Telemetry Data
* Get Data
*
* Returns the first record found or creates a
* new record.
*
* @returns Promise<InstallationAdmin>
*/
async getTelemetryData(): Promise<InstallationAdmin> {
async getData(): Promise<InstallationAdmin> {
const repo = await this.getInstallationAdminRepo();
const [record] = await repo.find();

Expand All @@ -55,4 +49,14 @@ export class TypeORMInstallationAdminImpl implements InstallationAdminDB {
/* Record not found - create one */
return this.createDefaultRecord();
}

async setSettings(settings: InstallationAdminSettings): Promise<void> {
const record = await this.getData();
record.settings = {
...settings,
}

const repo = await this.getInstallationAdminRepo();
await repo.save(record);
}
}
10 changes: 7 additions & 3 deletions components/gitpod-protocol/src/admin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { User, Workspace, NamedWorkspaceFeatureFlag } from "./protocol";
import { WorkspaceInstance, WorkspaceInstancePhase } from "./workspace-instance";
import { RoleOrPermission } from "./permission";
import { AccountStatement } from "./accounting-protocol";
import { InstallationAdminSettings } from "./installation-admin-protocol";

export interface AdminServer {
adminGetUsers(req: AdminGetListRequest<User>): Promise<AdminGetListResult<User>>;
Expand All @@ -29,6 +30,9 @@ export interface AdminServer {
adminIsStudent(userId: string): Promise<boolean>;
adminAddStudentEmailDomain(userId: string, domain: string): Promise<void>;
adminGrantExtraHours(userId: string, extraHours: number): Promise<void>;

adminGetSettings(): Promise<InstallationAdminSettings>
adminUpdateSettings(settings: InstallationAdminSettings): Promise<void>
}

export interface AdminGetListRequest<T> {
Expand Down Expand Up @@ -65,7 +69,7 @@ export interface AdminModifyPermanentWorkspaceFeatureFlagRequest {
}[]
}

export interface WorkspaceAndInstance extends Omit<Workspace, "id"|"creationTime">, Omit<WorkspaceInstance, "id"|"creationTime"> {
export interface WorkspaceAndInstance extends Omit<Workspace, "id" | "creationTime">, Omit<WorkspaceInstance, "id" | "creationTime"> {
workspaceId: string;
workspaceCreationTime: string;
instanceId: string;
Expand All @@ -78,7 +82,7 @@ export namespace WorkspaceAndInstance {
return {
id: wai.workspaceId,
creationTime: wai.workspaceCreationTime,
... wai
...wai
};
}

Expand All @@ -89,7 +93,7 @@ export namespace WorkspaceAndInstance {
return {
id: wai.instanceId,
creationTime: wai.instanceCreationTime,
... wai
...wai
};
}
}
Expand Down
5 changes: 5 additions & 0 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { GithubUpgradeURL, PlanCoupon } from './payment-protocol';
import { TeamSubscription, TeamSubscriptionSlot, TeamSubscriptionSlotResolved } from './team-subscription-protocol';
import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from './analytics';
import { IDEServer } from './ide-protocol';
import { InstallationAdminSettings } from './installation-admin-protocol';

export interface GitpodClient {
onInstanceUpdate(instance: WorkspaceInstance): void;
Expand Down Expand Up @@ -131,6 +132,10 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
resetGenericInvite(inviteId: string): Promise<TeamMembershipInvite>;
deleteTeam(teamId: string, userId: string): Promise<void>;

// Admin Settings
adminGetSettings(): Promise<InstallationAdminSettings>;
adminUpdateSettings(settings: InstallationAdminSettings): Promise<void>;

// Projects
getProviderRepositoriesForUser(params: GetProviderRepositoriesParams): Promise<ProviderRepository[]>;
createProject(params: CreateProjectParams): Promise<Project>;
Expand Down
27 changes: 24 additions & 3 deletions components/gitpod-protocol/src/installation-admin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,32 @@
* See License-AGPL.txt in the project root for license information.
*/

export interface InstallationAdminSettings {
sendTelemetry: boolean;
import { v4 as uuidv4 } from 'uuid';

const InstallationAdminSettingsPrototype = {
sendTelemetry: true
}

export type InstallationAdminSettings = typeof InstallationAdminSettingsPrototype;

export namespace InstallationAdminSettings {
export function fields(): (keyof InstallationAdminSettings)[] {
return Object.keys(InstallationAdminSettingsPrototype) as (keyof InstallationAdminSettings)[];
}
}

export interface InstallationAdmin {
id: string;
settings: InstallationAdminSettings;
}
}

export namespace InstallationAdmin {
export function createDefault(): InstallationAdmin {
return {
id: uuidv4(),
settings: {
...InstallationAdminSettingsPrototype,
}
};
}
}
11 changes: 1 addition & 10 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { injectable, inject } from "inversify";
import { GitpodServerImpl, traceAPIParams, traceWI, censor } from "../../../src/workspace/gitpod-server-impl";
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
import { GitpodServer, GitpodClient, AdminGetListRequest, User, AdminGetListResult, Permission, AdminBlockUserRequest, AdminModifyRoleOrPermissionRequest, RoleOrPermission, AdminModifyPermanentWorkspaceFeatureFlagRequest, UserFeatureSettings, AdminGetWorkspacesRequest, WorkspaceAndInstance, GetWorkspaceTimeoutResult, WorkspaceTimeoutDuration, WorkspaceTimeoutValues, SetWorkspaceTimeoutResult, WorkspaceContext, CreateWorkspaceMode, WorkspaceCreationResult, PrebuiltWorkspaceContext, CommitContext, PrebuiltWorkspace, PermissionName, WorkspaceInstance, EduEmailDomain, ProviderRepository, Queue, PrebuildWithStatus, CreateProjectParams, Project, StartPrebuildResult, ClientHeaderFields, Workspace } from "@gitpod/gitpod-protocol";
import { GitpodServer, GitpodClient, AdminGetListRequest, User, AdminGetListResult, Permission, AdminBlockUserRequest, AdminModifyRoleOrPermissionRequest, RoleOrPermission, AdminModifyPermanentWorkspaceFeatureFlagRequest, UserFeatureSettings, AdminGetWorkspacesRequest, WorkspaceAndInstance, GetWorkspaceTimeoutResult, WorkspaceTimeoutDuration, WorkspaceTimeoutValues, SetWorkspaceTimeoutResult, WorkspaceContext, CreateWorkspaceMode, WorkspaceCreationResult, PrebuiltWorkspaceContext, CommitContext, PrebuiltWorkspace, WorkspaceInstance, EduEmailDomain, ProviderRepository, Queue, PrebuildWithStatus, CreateProjectParams, Project, StartPrebuildResult, ClientHeaderFields, Workspace } from "@gitpod/gitpod-protocol";
import { ResponseError } from "vscode-jsonrpc";
import { TakeSnapshotRequest, AdmissionLevel, ControlAdmissionRequest, StopWorkspacePolicy, DescribeWorkspaceRequest, SetTimeoutRequest } from "@gitpod/ws-manager/lib";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
Expand Down Expand Up @@ -602,15 +602,6 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
});
}

protected async guardAdminAccess(method: string, params: any, requiredPermission: PermissionName) {
const user = this.checkAndBlockUser(method);
if (!this.authorizationService.hasPermission(user, requiredPermission)) {
log.warn({ userId: this.user?.id }, "unauthorised admin access", { authorised: false, method, params });
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed");
}
log.info({ userId: this.user?.id }, "admin access", { authorised: true, method, params });
}

protected async findPrebuiltWorkspace(parentCtx: TraceContext, user: User, context: WorkspaceContext, mode: CreateWorkspaceMode): Promise<WorkspaceCreationResult | PrebuiltWorkspaceContext | undefined> {
const ctx = TraceContext.childContext("findPrebuiltWorkspace", parentCtx);

Expand Down
2 changes: 2 additions & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
"adminForceStopWorkspace": { group: "default", points: 1 },
"adminRestoreSoftDeletedWorkspace": { group: "default", points: 1 },
"adminSetLicense": { group: "default", points: 1 },
"adminGetSettings": { group: "default", points: 1 },
"adminUpdateSettings": { group: "default", points: 1 },

"validateLicense": { group: "default", points: 1 },
"getLicenseInfo": { group: "default", points: 1 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export class InstallationAdminController {
const app = express();

app.get('/data', async (req: express.Request, res: express.Response) => {
const data = await this.installationAdminDb.getTelemetryData();
const data = await this.installationAdminDb.getData();

res.send(data);
res.status(200).json(data);
});

return app;
Expand Down
Loading

0 comments on commit f9ec0f8

Please sign in to comment.