Skip to content
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

[dashboard] Fix remaining issues in new start screen #3693

Merged
merged 12 commits into from
Apr 2, 2021
Merged
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
2 changes: 1 addition & 1 deletion components/dashboard/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function Modal(props: {
return (
<div className="fixed top-0 left-0 bg-black bg-opacity-70 z-50 w-screen h-screen" onClick={props.onClose}>
<div className="w-screen h-screen align-middle" style={{display: 'table-cell'}}>
<div className={"relative bg-white border rounded-xl p-6 max-w-lg mx-auto text-gray-600" + props.className} onClick={e => e.stopPropagation()}>
<div className={"relative bg-white border rounded-xl p-6 max-w-lg mx-auto text-left text-gray-600 " + (props.className || '')} onClick={e => e.stopPropagation()}>
{props.closeable !== false && (
<div className="absolute right-7 top-6 cursor-pointer hover:bg-gray-200 rounded-md p-2" onClick={props.onClose}>
<svg version="1.1" width="14px" height="14px"
Expand Down
3 changes: 3 additions & 0 deletions components/dashboard/src/icons/CaretDown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion components/dashboard/src/settings/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function Account() {

<SettingsPage title='Account' subtitle='Manage account and git configuration.'>
<h3>Profile</h3>
<p className="text-base text-gray-500 pb-4">The following information will be used to set up git configuration. You can override git author name and email per project by using the default environment variables <span className="text-gray--300 bg-gray-100 px-1.5 py-1 rounded-md text-sm font-mono font-medium">GIT_AUTHOR_NAME</span> and <span className="text-gray--300 bg-gray-100 px-1.5 py-1 rounded-md text-sm font-mono font-medium">GIT_COMMITTER_EMAIL</span>.</p>
<p className="text-base text-gray-500 pb-4 max-w-2xl">The following information will be used to set up git configuration. You can override git author name and email per project by using the default environment variables <span className="text-gray--300 bg-gray-100 px-1.5 py-1 rounded-md text-sm font-mono font-medium">GIT_AUTHOR_NAME</span> and <span className="text-gray--300 bg-gray-100 px-1.5 py-1 rounded-md text-sm font-mono font-medium">GIT_COMMITTER_EMAIL</span>.</p>
<div className="flex flex-col lg:flex-row">
<div>
<div className="mt-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function AddEnvVarModal(p: EnvVarModalProps) {
return <Modal visible={true} onClose={p.onClose} onEnter={save}>
<h3 className="mb-4">{isNew ? 'New' : 'Edit'} Variable</h3>
<div className="border-t border-b -mx-6 px-6 py-4 flex flex-col">
{error ? <div className="bg-gitpod-kumquat-light rounded-md p-3 text-red-500 text-sm mb-2">
{error ? <div className="bg-gitpod-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2">
{error}
</div> : null}
<div>
Expand Down
31 changes: 7 additions & 24 deletions components/dashboard/src/settings/Plans.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ export default function () {
const { user } = useContext(UserContext);
const { server } = getGitpodService();
const [ accountStatement, setAccountStatement ] = useState<AccountStatement>();
const [ showPaymentUI, setShowPaymentUI ] = useState<boolean>();
const [ isChargebeeCustomer, setIsChargebeeCustomer ] = useState<boolean>();
const [ isStudent, setIsStudent ] = useState<boolean>();
const [ clientRegion, setClientRegion ] = useState<string>();
const [ currency, setCurrency ] = useState<Currency>('USD');
const [ availableCoupons, setAvailableCoupons ] = useState<PlanCoupon[]>();
const [ appliedCoupons, setAppliedCoupons ] = useState<PlanCoupon[]>();
Expand All @@ -40,11 +38,9 @@ export default function () {
useEffect(() => {
Promise.all([
server.getAccountStatement({}).then(v => () => setAccountStatement(v)),
server.getShowPaymentUI().then(v => () => setShowPaymentUI(v)),
server.isChargebeeCustomer().then(v => () => setIsChargebeeCustomer(v)),
server.isStudent().then(v => () => setIsStudent(v)),
server.getClientRegion().then(v => () => {
setClientRegion(v);
// @ts-ignore
setCurrency(countries[v]?.currency === 'EUR' ? 'EUR' : 'USD');
}),
Expand All @@ -58,14 +54,6 @@ export default function () {
}
}, []);

console.log('accountStatement', accountStatement);
console.log('showPaymentUI', showPaymentUI);
console.log('isChargebeeCustomer', isChargebeeCustomer);
console.log('isStudent', isStudent);
console.log('clientRegion', clientRegion);
console.log('availableCoupons', availableCoupons);
console.log('appliedCoupons', appliedCoupons);
console.log('gitHubUpgradeUrls', gitHubUpgradeUrls);
console.log('privateRepoTrialEndDate', privateRepoTrialEndDate);

const activeSubscriptions = (accountStatement?.subscriptions || []).filter(s => Subscription.isActive(s, new Date().toISOString()));
Expand Down Expand Up @@ -93,8 +81,8 @@ export default function () {
if (paidPlan?.chargebeeId === pendingUpgradePlan.chargebeeId) {
// The upgrade already worked
removePendingUpgrade();
} else if ((pendingUpgradePlan.pendingSince + 1000 * 60 * 3) < Date.now()) {
// Pending upgrades expire after 3 minutes
} else if ((pendingUpgradePlan.pendingSince + 1000 * 60 * 5) < Date.now()) {
// Pending upgrades expire after 5 minutes
removePendingUpgrade();
} else if (!pollAccountStatementTimeout) {
// Refresh account statement in 10 seconds in order to poll for upgrade confirmed
Expand All @@ -104,17 +92,14 @@ export default function () {
}, 10000);
}
}
console.log('pendingUpgradePlan', pendingUpgradePlan);

// Optimistically select a new paid plan even if the transaction is still in progress (i.e. waiting for Chargebee callback)
const currentPlan = pendingUpgradePlan || paidPlan || freePlan;
console.log('currentPlan', currentPlan);

// If the user has a paid plan with a different currency, force that currency.
if (currency !== currentPlan.currency && !Plans.isFreePlan(currentPlan.chargebeeId)) {
setCurrency(currentPlan.currency);
}
console.log('currency', currency);

const personalPlan = Plans.getPersonalPlan(currency);
const professionalPlan = Plans.getNewProPlan(currency);
Expand All @@ -124,7 +109,6 @@ export default function () {
const scheduledDowngradePlanId = !!(paidSubscription?.paymentData?.downgradeDate)
? paidSubscription.paymentData.newPlan || personalPlan.chargebeeId
: undefined;
console.log('scheduledDowngradePlanId', scheduledDowngradePlanId);

const [ pendingDowngradePlan, setPendingDowngradePlan ] = useState<PendingPlan | undefined>(getLocalStorageObject('pendingDowngradePlan'));
const setPendingDowngrade = (to: PendingPlan) => {
Expand All @@ -144,8 +128,8 @@ export default function () {
} else if (scheduledDowngradePlanId === pendingDowngradePlan.chargebeeId) {
// The Downgrade is already scheduled
removePendingDowngrade();
} else if ((pendingDowngradePlan.pendingSince + 1000 * 60 * 3) < Date.now()) {
// Pending downgrades expire after 3 minutes
} else if ((pendingDowngradePlan.pendingSince + 1000 * 60 * 5) < Date.now()) {
// Pending downgrades expire after 5 minutes
removePendingDowngrade();
} else if (!pollAccountStatementTimeout) {
// Refresh account statement in 10 seconds in orer to poll for downgrade confirmed/scheduled
Expand All @@ -155,7 +139,6 @@ export default function () {
}, 10000);
}
}
console.log('pendingDowngradePlan', pendingDowngradePlan);

const [ confirmUpgradeToPlan, setConfirmUpgradeToPlan ] = useState<Plan>();
const [ confirmDowngradeToPlan, setConfirmDowngradeToPlan ] = useState<Plan>();
Expand Down Expand Up @@ -330,7 +313,7 @@ export default function () {
const bottomLabel = ('pendingSince' in currentPlan) ? <p className="text-green-600 animate-pulse">Upgrade in progress</p> : undefined;
planCards.push(<PlanCard plan={applyCoupons(unleashedPlan, appliedCoupons)} isCurrent={true} bottomLabel={bottomLabel}>{unleashedFeatures}</PlanCard>);
} else {
const targetPlan = applyCoupons(unleashedPlan, availableCoupons);
const targetPlan = applyCoupons(isStudent ? studentUnleashedPlan : unleashedPlan, availableCoupons);
let onUpgrade;
switch (Plans.subscriptionChange(currentPlan.type, targetPlan.type)) {
case 'upgrade': onUpgrade = () => confirmUpgrade(targetPlan); break;
Expand Down Expand Up @@ -364,7 +347,7 @@ export default function () {
</div>
{!!confirmUpgradeToPlan && <Modal visible={true} onClose={() => setConfirmUpgradeToPlan(undefined)}>
<h3>Upgrade to {confirmUpgradeToPlan.name}</h3>
<div className="border-t border-b border-gray-200 mt-2 -mx-6 px-6 py-2">
<div className="border-t border-b border-gray-200 mt-4 -mx-6 px-6 py-2">
<p className="mt-1 mb-4 text-base">You are about to upgrade to {confirmUpgradeToPlan.name}.</p>
{!Plans.isFreePlan(currentPlan.chargebeeId) && <div className="flex rounded-md bg-gray-200 p-4 mb-4">
<img className="w-4 h-4 m-1 ml-2 mr-4" src={info} />
Expand All @@ -381,7 +364,7 @@ export default function () {
</Modal>}
{!!confirmDowngradeToPlan && <Modal visible={true} onClose={() => setConfirmDowngradeToPlan(undefined)}>
<h3>Downgrade to {confirmDowngradeToPlan.name}</h3>
<div className="border-t border-b border-gray-200 mt-2 -mx-6 px-6 py-2">
<div className="border-t border-b border-gray-200 mt-4 -mx-6 px-6 py-2">
<p className="mt-1 mb-4 text-base">You are about to downgrade to {confirmDowngradeToPlan.name}.</p>
<div className="flex rounded-md bg-gray-200 p-4 mb-4">
<img className="w-4 h-4 m-1 ml-2 mr-4" src={info} />
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/settings/Teams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ function AllTeams() {
</div>
)}
</div>
<div className="my-auto flex w-1/12 pl-8 opacity-0 group-hover:opacity-100">
<div className="my-auto flex w-1/12 pl-8 mr-4 opacity-0 group-hover:opacity-100">
<div className="self-center hover:bg-gray-200 rounded-md cursor-pointer w-8">
<ContextMenu menuEntries={subscriptionMenu(sub)}>
<img className="w-8 h-8 p-1" src={ThreeDots} alt="Actions" />
Expand Down
82 changes: 54 additions & 28 deletions components/dashboard/src/start/CreateWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
*/

import EventEmitter from "events";
import React, { useEffect, Suspense } from "react";
import React, { useEffect, Suspense, useContext } from "react";
import { CreateWorkspaceMode, WorkspaceCreationResult, RunningWorkspacePrebuildStarting } from "@gitpod/gitpod-protocol";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import Modal from "../components/Modal";
import { getGitpodService, gitpodHostUrl } from "../service/service";
import { StartPage, StartPhase } from "./StartPage";
import { UserContext } from "../user-context";
import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage";
import StartWorkspace from "./StartWorkspace";

const WorkspaceLogs = React.lazy(() => import('./WorkspaceLogs'));
Expand All @@ -21,16 +22,10 @@ export interface CreateWorkspaceProps {

export interface CreateWorkspaceState {
result?: WorkspaceCreationResult;
error?: CreateWorkspaceError;
error?: StartWorkspaceError;
stillParsing: boolean;
}

export interface CreateWorkspaceError {
message?: string;
code?: number;
data?: any;
}

export default class CreateWorkspace extends React.Component<CreateWorkspaceProps, CreateWorkspaceState> {

constructor(props: CreateWorkspaceProps) {
Expand All @@ -44,10 +39,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp

async createWorkspace(mode = CreateWorkspaceMode.SelectIfRunning) {
// Invalidate any previous result.
this.setState({
result: undefined,
stillParsing: true,
});
this.setState({ result: undefined, stillParsing: true });

// We assume anything longer than 3 seconds is no longer just parsing the context URL (i.e. it's now creating a workspace).
let timeout = setTimeout(() => this.setState({ stillParsing: false }), 3000);
Expand All @@ -62,17 +54,11 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
return;
}
clearTimeout(timeout);
this.setState({
result,
stillParsing: false,
});
this.setState({ result, stillParsing: false });
} catch (error) {
clearTimeout(timeout);
console.error(error);
this.setState({
error,
stillParsing: false,
});
this.setState({ error, stillParsing: false });
}
}

Expand All @@ -81,22 +67,45 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
let phase = StartPhase.Checking;
let statusMessage = <p className="text-base text-gray-400">{this.state.stillParsing ? 'Parsing context …' : 'Preparing workspace …'}</p>;

const error = this.state?.error;
let error = this.state?.error;
if (error) {
switch (error.code) {
case ErrorCodes.CONTEXT_PARSE_ERROR:
statusMessage = <div className="text-center">
<p className="text-base text-red-500">Unrecognized context: '{contextUrl}'</p>
<p className="text-base mt-2">Learn more about <a className="text-blue" href="https://www.gitpod.io/docs/context-urls/">supported context URLs</a></p>
</div>;
break;
case ErrorCodes.NOT_FOUND:
statusMessage = <div className="text-center">
<p className="text-base text-red-500">Not found: {contextUrl}</p>
<p className="text-base text-gitpod-red">Not found: {contextUrl}</p>
</div>;
break;
case ErrorCodes.PLAN_DOES_NOT_ALLOW_PRIVATE_REPOS:
// HACK: Hide the error (behind the modal)
error = undefined;
phase = StartPhase.Stopped;
statusMessage = <LimitReachedModal>
<p className="mt-1 mb-2 text-base">Gitpod is free for public repositories. To work with private repositories, please upgrade to a compatible paid plan.</p>
</LimitReachedModal>;
break;
case ErrorCodes.TOO_MANY_RUNNING_WORKSPACES:
// HACK: Hide the error (behind the modal)
error = undefined;
phase = StartPhase.Stopped;
statusMessage = <LimitReachedModal>
<p className="mt-1 mb-2 text-base">You have reached the limit of parallel running workspaces for your account. Please, upgrade or stop one of the running workspaces.</p>
</LimitReachedModal>;
break;
case ErrorCodes.NOT_ENOUGH_CREDIT:
// HACK: Hide the error (behind the modal)
error = undefined;
phase = StartPhase.Stopped;
statusMessage = <LimitReachedModal>
<p className="mt-1 mb-2 text-base">You have reached the limit of monthly workspace hours for your account. Please upgrade to get more hours for your workspaces.</p>
</LimitReachedModal>;
break;
default:
statusMessage = <p className="text-base text-red-500 w-96">Unknown Error: {JSON.stringify(this.state?.error, null, 2)}</p>;
statusMessage = <p className="text-base text-gitpod-red w-96">Unknown Error: {JSON.stringify(this.state?.error, null, 2)}</p>;
break;
}
}
Expand All @@ -109,7 +118,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
else if (result?.existingWorkspaces) {
statusMessage = <Modal visible={true} closeable={false} onClose={()=>{}}>
<h3>Running Workspaces</h3>
<div className="border-t border-b border-gray-200 mt-2 -mx-6 px-6 py-2">
<div className="border-t border-b border-gray-200 mt-4 -mx-6 px-6 py-2">
<p className="mt-1 mb-2 text-base">You already have running workspaces with the same context. You can open an existing one or open a new workspace.</p>
<>
{result?.existingWorkspaces?.map(w =>
Expand All @@ -136,10 +145,10 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
/>;
}

return <StartPage phase={phase} error={!!error}>
return <StartPage phase={phase} error={error}>
{statusMessage}
{error && <div>
<a href={gitpodHostUrl.asDashboard().toString()}><button className="mt-8 secondary">Go back to dashboard</button></a>
<a href={gitpodHostUrl.asDashboard().toString()}><button className="mt-8 secondary">Go to Dashboard</button></a>
<p className="mt-14 text-base text-gray-400 flex space-x-2">
<a href="https://www.gitpod.io/docs/">Docs</a>
<span>—</span>
Expand All @@ -152,6 +161,23 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
}
}

function LimitReachedModal(p: { children: React.ReactNode }) {
const { user } = useContext(UserContext);
return <Modal visible={true} closeable={false} onClose={()=>{}}>
<h3 className="flex">
<span className="flex-grow">Limit Reached</span>
<img className="rounded-full w-8 h-8" src={user?.avatarUrl || ''} alt={user?.name || 'Anonymous'} />
</h3>
<div className="border-t border-b border-gray-200 mt-4 -mx-6 px-6 py-2">
{p.children}
</div>
<div className="flex justify-end mt-6">
<a href={gitpodHostUrl.asDashboard().toString()}><button className="secondary">Go to Dashboard</button></a>
<a href={gitpodHostUrl.with({ pathname: 'plans' }).toString()} className="ml-2"><button>Upgrade</button></a>
</div>
</Modal>;
}

interface RunningPrebuildViewProps {
runningPrebuild: {
prebuildID: string
Expand Down
21 changes: 18 additions & 3 deletions components/dashboard/src/start/StartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,36 @@ function ProgressBar(props: { phase: number, error: boolean }) {

export interface StartPageProps {
phase?: number;
error?: boolean;
error?: StartWorkspaceError;
title?: string;
children?: React.ReactNode;
}

export interface StartWorkspaceError {
message?: string;
code?: number;
data?: any;
}

export function StartPage(props: StartPageProps) {
const { phase, error } = props;
let title = props.title || getPhaseTitle(phase, error);
let title = props.title || getPhaseTitle(phase, !!error);
return <div className="w-screen h-screen bg-white align-middle">
<div className="flex flex-col mx-auto items-center h-screen">
<div className="flex flex-col mx-auto items-center text-center h-screen">
jankeromnes marked this conversation as resolved.
Show resolved Hide resolved
<div className="h-1/3"></div>
<img src={gitpodIcon} className={`h-16 flex-shrink-0 ${(error || phase === StartPhase.Stopped) ? '' : 'animate-bounce'}`} />
<h3 className="mt-8 text-xl">{title}</h3>
{typeof(phase) === 'number' && phase < StartPhase.Stopping && <ProgressBar phase={phase} error={!!error} />}
{error && <StartError error={error} />}
{props.children}
</div>
</div>;
}

function StartError(props: { error: StartWorkspaceError }) {
const { error } = props;
if (!error) {
return null;
}
return <p className="text-base text-gitpod-red">{error.message}</p>;
}
Loading