mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
Enable Dedicated Onboarding Flow – WEB-193 (#17303)
* uncommitted yarn.lock * [protocol] adding ONBOARDING_IN_PROGRESS * [gitpod-db] add `someOrgWithSSO` to team db * [server] add ONBOARDING_IN_PROGRESS hook * [dashboard] catch ONBOARDING_IN_PROGRESS error * [configcat] adding `custom.gitpod_host` attribute to select preview envs
This commit is contained in:
parent
dafcf19ded
commit
ee82d153e3
54
components/dashboard/src/DedicatedOnboarding.tsx
Normal file
54
components/dashboard/src/DedicatedOnboarding.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2023 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 { useCallback, useState } from "react";
|
||||
import { Button } from "./components/Button";
|
||||
import Modal, { ModalBody, ModalFooter, ModalHeader } from "./components/Modal";
|
||||
import { OIDCClientConfigModal } from "./teams/sso/OIDCClientConfigModal";
|
||||
|
||||
type Props = {
|
||||
onComplete?: () => void;
|
||||
};
|
||||
export default function DedicatedOnboarding({ onComplete }: Props) {
|
||||
const [showModal] = useState<boolean>(false);
|
||||
|
||||
const acceptAndContinue = useCallback(() => {
|
||||
// // setShowModal(true);
|
||||
}, []);
|
||||
|
||||
// const onAuthorize = useCallback(
|
||||
// (payload?: string) => {
|
||||
// onComplete && onComplete();
|
||||
|
||||
// // run without await, so the integrated closing of new tab isn't blocked
|
||||
// (async () => {
|
||||
// window.location.href = gitpodHostUrl.asDashboard().toString();
|
||||
// })();
|
||||
// },
|
||||
// [onComplete],
|
||||
// );
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!showModal && (
|
||||
<Modal visible={true} onClose={() => {}} closeable={false}>
|
||||
<ModalHeader>Welcome to Gitpod Dedicated 🚀</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className="pb-4 text-gray-500 text-base">
|
||||
Before this instance can be used, you need to set up Single Sign-on.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button className={"ml-2"} onClick={acceptAndContinue}>
|
||||
Continue
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)}
|
||||
{showModal && <OIDCClientConfigModal onClose={() => {}} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -15,6 +15,7 @@ import { isGitpodIo } from "../../utils";
|
||||
import { CaughtError } from "./ReloadPageErrorBoundary";
|
||||
|
||||
const Setup = lazy(() => import(/* webpackPrefetch: true */ "../../Setup"));
|
||||
const DedicatedOnboarding = lazy(() => import(/* webpackPrefetch: true */ "../../DedicatedOnboarding"));
|
||||
|
||||
// Error boundary intended to catch and handle expected errors from api calls
|
||||
export const QueryErrorBoundary: FC = ({ children }) => {
|
||||
@ -50,6 +51,15 @@ const ExpectedQueryErrorsFallback: FC<FallbackProps> = ({ error, resetErrorBound
|
||||
return <Login onLoggedIn={resetErrorBoundary} />;
|
||||
}
|
||||
|
||||
// Setup needed before proceeding
|
||||
if (caughtError.code === ErrorCodes.ONBOARDING_IN_PROGRESS) {
|
||||
return (
|
||||
<Suspense fallback={<AppLoading />}>
|
||||
<DedicatedOnboarding onComplete={resetErrorBoundary} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// Setup needed before proceeding
|
||||
if (caughtError.code === ErrorCodes.SETUP_REQUIRED) {
|
||||
return (
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
import * as chai from "chai";
|
||||
const expect = chai.expect;
|
||||
import { suite, test, timeout } from "mocha-typescript";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { testContainer } from "./test-container";
|
||||
import { TeamDBImpl } from "./typeorm/team-db-impl";
|
||||
@ -16,6 +17,7 @@ import { DBTeam } from "./typeorm/entity/db-team";
|
||||
import { DBTeamMembership } from "./typeorm/entity/db-team-membership";
|
||||
import { DBUser } from "./typeorm/entity/db-user";
|
||||
import { DBIdentity } from "./typeorm/entity/db-identity";
|
||||
import { Connection } from "typeorm";
|
||||
|
||||
@suite
|
||||
class TeamDBSpec {
|
||||
@ -37,6 +39,7 @@ class TeamDBSpec {
|
||||
await manager.getRepository(DBTeamMembership).delete({});
|
||||
await manager.getRepository(DBUser).delete({});
|
||||
await manager.getRepository(DBIdentity).delete({});
|
||||
await manager.query("delete from d_b_oidc_client_config;");
|
||||
}
|
||||
|
||||
@test(timeout(10000))
|
||||
@ -151,6 +154,31 @@ class TeamDBSpec {
|
||||
const result = await this.db.findTeams(0, 10, "creationTime", "DESC", searchTerm);
|
||||
expect(result.rows.length).to.eq(1);
|
||||
}
|
||||
|
||||
@test(timeout(10000))
|
||||
public async test_someOrgWithSSOExists() {
|
||||
expect(await this.db.someOrgWithSSOExists(), "case 1: empty db").to.be.false;
|
||||
|
||||
const user = await this.userDb.newUser();
|
||||
const org = await this.db.createTeam(user.id, "Some Org");
|
||||
expect(await this.db.someOrgWithSSOExists(), "case 2: org without sso").to.be.false;
|
||||
|
||||
await this.exec(async (c) => {
|
||||
await c.query("INSERT INTO d_b_oidc_client_config (id, issuer, organizationId, data) VALUES (?,?,?,?)", [
|
||||
uuidv4(),
|
||||
"https://issuer.local",
|
||||
org.id,
|
||||
"{}",
|
||||
]);
|
||||
});
|
||||
expect(await this.db.someOrgWithSSOExists(), "case 3: org with sso").to.be.true;
|
||||
}
|
||||
|
||||
protected async exec(queryFn: (connection: Connection) => Promise<void>) {
|
||||
const typeorm = testContainer.get<TypeORM>(TypeORM);
|
||||
const connection = await typeorm.getConnection();
|
||||
await queryFn(connection);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TeamDBSpec();
|
||||
|
||||
@ -40,4 +40,6 @@ export interface TeamDB {
|
||||
|
||||
findOrgSettings(teamId: string): Promise<OrganizationSettings | undefined>;
|
||||
setOrgSettings(teamId: string, settings: Partial<OrganizationSettings>): Promise<void>;
|
||||
|
||||
someOrgWithSSOExists(): Promise<boolean>;
|
||||
}
|
||||
|
||||
@ -365,4 +365,12 @@ export class TeamDBImpl implements TeamDB {
|
||||
repo.save(team);
|
||||
}
|
||||
}
|
||||
|
||||
public async someOrgWithSSOExists(): Promise<boolean> {
|
||||
const repo = await this.getOrgSettingsRepo();
|
||||
const result = await repo.query(
|
||||
`select org.id from d_b_team as org inner join d_b_oidc_client_config as oidc on org.id = oidc.organizationId limit 1;`,
|
||||
);
|
||||
return result.length === 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,9 @@ const (
|
||||
// 411 No User
|
||||
NEEDS_VERIFICATION = 411
|
||||
|
||||
// 412 Dedicated Cell is in "onboarding" mode
|
||||
ONBOARDING_IN_PROGRESS = 412
|
||||
|
||||
// 429 Too Many Requests
|
||||
TOO_MANY_REQUESTS = 429
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ export const PROJECT_ID_ATTRIBUTE = "project_id";
|
||||
export const TEAM_ID_ATTRIBUTE = "team_id";
|
||||
export const TEAM_NAME_ATTRIBUTE = "team_name";
|
||||
export const BILLING_TIER_ATTRIBUTE = "billing_tier";
|
||||
export const GITPOD_HOST = "gitpod_host";
|
||||
|
||||
export class ConfigCatClient implements Client {
|
||||
private client: IConfigCatClient;
|
||||
@ -51,6 +52,9 @@ export function attributesToUser(attributes: Attributes): ConfigCatUser {
|
||||
if (attributes.billingTier) {
|
||||
custom[BILLING_TIER_ATTRIBUTE] = attributes.billingTier;
|
||||
}
|
||||
if (attributes.gitpodHost) {
|
||||
custom[GITPOD_HOST] = attributes.gitpodHost;
|
||||
}
|
||||
|
||||
return new ConfigCatUser(userId, email, "", custom);
|
||||
}
|
||||
|
||||
@ -22,8 +22,12 @@ export interface Attributes {
|
||||
|
||||
// Currently selected Gitpod Team ID (mapped to "custom.team_id")
|
||||
teamId?: string;
|
||||
|
||||
// Currently selected Gitpod Team Name (mapped to "custom.team_name")
|
||||
teamName?: string;
|
||||
|
||||
// Host name of the Gitpod installation (mapped to "custom.gitpod_host")
|
||||
gitpodHost?: string;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
|
||||
@ -26,6 +26,9 @@ export namespace ErrorCodes {
|
||||
// 411 No User
|
||||
export const NEEDS_VERIFICATION = 411;
|
||||
|
||||
// 412 Dedicated Cell is in "onboarding" mode
|
||||
export const ONBOARDING_IN_PROGRESS = 412;
|
||||
|
||||
// 429 Too Many Requests
|
||||
export const TOO_MANY_REQUESTS = 429;
|
||||
|
||||
|
||||
@ -357,9 +357,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
|
||||
}
|
||||
|
||||
protected checkUser(methodName?: string, logPayload?: {}, ctx?: LogContext): User {
|
||||
if (this.showSetupCondition?.value) {
|
||||
// Old self-hosted mode did not require a session, therefore it's checked first.
|
||||
// But we need to make sure, it's not triggered if the new Dedicated Onboarding is active.
|
||||
if (this.showSetupCondition?.value && !this.showOnboardingFlowCondition?.value) {
|
||||
throw new ResponseError(ErrorCodes.SETUP_REQUIRED, "Setup required.");
|
||||
}
|
||||
|
||||
// Generally, a user session is required.
|
||||
if (!this.user) {
|
||||
throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "User is not authenticated. Please login.");
|
||||
}
|
||||
@ -377,6 +381,12 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
|
||||
}
|
||||
log.debug(userContext, methodName, payload);
|
||||
}
|
||||
|
||||
// A session is required to continue with an activated onboarding flow.
|
||||
if (this.showOnboardingFlowCondition?.value) {
|
||||
throw new ResponseError(ErrorCodes.ONBOARDING_IN_PROGRESS, "Dedicated Onboarding Flow in progress.");
|
||||
}
|
||||
|
||||
return this.user;
|
||||
}
|
||||
|
||||
@ -403,7 +413,26 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
|
||||
}
|
||||
|
||||
protected showSetupCondition: { value: boolean } | undefined = undefined;
|
||||
protected showOnboardingFlowCondition: { value: boolean } | undefined = undefined;
|
||||
protected async doUpdateUser(): Promise<void> {
|
||||
// Conditionally enable Dedicated Onboarding Flow
|
||||
const enableDedicatedOnboardingFlow = await this.configCatClientFactory().getValueAsync(
|
||||
"enableDedicatedOnboardingFlow",
|
||||
false,
|
||||
{
|
||||
gitpodHost: new URL(this.config.hostUrl.toString()).host,
|
||||
},
|
||||
);
|
||||
if (enableDedicatedOnboardingFlow) {
|
||||
const someOrgWithSSOExists = await this.teamDB.someOrgWithSSOExists();
|
||||
const shouldShowOnboardingFlow = !someOrgWithSSOExists;
|
||||
this.showOnboardingFlowCondition = { value: shouldShowOnboardingFlow };
|
||||
if (shouldShowOnboardingFlow) {
|
||||
console.log(`Dedicated Onboarding flow is active.`);
|
||||
}
|
||||
} else {
|
||||
this.showOnboardingFlowCondition = { value: false };
|
||||
}
|
||||
// execute the check for the setup to be shown until the setup is not required.
|
||||
// cf. evaluation of the condition in `checkUser`
|
||||
if (!this.config.showSetupModal) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user