Skip to content
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
83 changes: 66 additions & 17 deletions components/dashboard/src/AppNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { useOrgBillingMode } from "./data/billing-mode/org-billing-mode-query";
import { Organization } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
import { MaintenanceModeBanner } from "./org-admin/MaintenanceModeBanner";
import { MaintenanceNotificationBanner } from "./org-admin/MaintenanceNotificationBanner";
import { useToast } from "./components/toasts/Toasts";
import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils";
import onaWordmark from "./images/ona-wordmark.svg";

const KEY_APP_DISMISSED_NOTIFICATIONS = "gitpod-app-notifications-dismissed";
Expand Down Expand Up @@ -129,37 +131,84 @@ const INVALID_BILLING_ADDRESS = (stripePortalUrl: string | undefined) => {
} as Notification;
};

const GITPOD_CLASSIC_SUNSET = {
id: "gitpod-classic-sunset",
type: "info" as AlertType,
preventDismiss: true, // This makes it so users can't dismiss the notification
message: (
<span className="text-md text-white font-semibold items-center justify-center">
Meet <img src={onaWordmark} alt="Ona" className="inline align-middle w-12 mb-0.5" draggable="false" /> | the
privacy-first software engineering agent |{" "}
<a href="https://ona.com/" target="_blank" rel="noreferrer" className="underline hover:no-underline">
Get early access
</a>
</span>
),
} as Notification;
const GITPOD_CLASSIC_SUNSET = (
user: User | undefined,
toast: any,
onaClicked: boolean,
handleOnaBannerClick: () => void,
) => {
return {
id: "gitpod-classic-sunset",
type: "info" as AlertType,
message: (
<span className="text-md text-white font-semibold items-center justify-center">
Meet <img src={onaWordmark} alt="Ona" className="inline align-middle w-12 mb-0.5" draggable="false" /> |
the privacy-first software engineering agent |{" "}
{!onaClicked ? (
<button
onClick={handleOnaBannerClick}
className="underline hover:no-underline cursor-pointer bg-transparent border-none text-white font-semibold"
>
Get early access
</button>
) : (
<a
href="https://www.gitpod.io/solutions/ai"
target="_blank"
rel="noreferrer"
className="underline hover:no-underline"
>
Learn more
</a>
)}
</span>
),
} as Notification;
};

export function AppNotifications() {
const [topNotification, setTopNotification] = useState<Notification | undefined>(undefined);
const [onaClicked, setOnaClicked] = useState(false);
const { user, loading } = useUserLoader();
const { mutateAsync } = useUpdateCurrentUserMutation();
const { toast } = useToast();

const currentOrg = useCurrentOrg().data;
const { data: billingMode } = useOrgBillingMode();

useEffect(() => {
const storedOnaData = localStorage.getItem("ona-banner-data");
if (storedOnaData) {
const { clicked } = JSON.parse(storedOnaData);
setOnaClicked(clicked || false);
}
}, []);

const handleOnaBannerClick = useCallback(() => {
const userEmail = user ? getPrimaryEmail(user) || "" : "";
trackEvent("waitlist_joined", { email: userEmail, feature: "Ona" });

setOnaClicked(true);
const existingData = localStorage.getItem("ona-banner-data");
const parsedData = existingData ? JSON.parse(existingData) : {};
localStorage.setItem("ona-banner-data", JSON.stringify({ ...parsedData, clicked: true }));

toast(
<div>
<div className="font-medium">You're on the waitlist</div>
<div className="text-sm opacity-80">We'll reach out to you soon.</div>
</div>,
);
}, [user, toast]);

useEffect(() => {
let ignore = false;

const updateNotifications = async () => {
const notifications = [];
if (!loading) {
if (isGitpodIo()) {
notifications.push(GITPOD_CLASSIC_SUNSET);
notifications.push(GITPOD_CLASSIC_SUNSET(user, toast, onaClicked, handleOnaBannerClick));
}

if (
Expand Down Expand Up @@ -193,7 +242,7 @@ export function AppNotifications() {
return () => {
ignore = true;
};
}, [loading, mutateAsync, user, currentOrg, billingMode]);
}, [loading, mutateAsync, user, currentOrg, billingMode, onaClicked, handleOnaBannerClick, toast]);

const dismissNotification = useCallback(() => {
if (!topNotification) {
Expand All @@ -213,7 +262,7 @@ export function AppNotifications() {
{topNotification && (
<Alert
type={topNotification.type}
closable={topNotification.id !== "gitpod-classic-sunset"} // Only show close button if it's not the sunset notification
closable={true}
onClose={() => {
if (!topNotification.preventDismiss) {
dismissNotification();
Expand Down
155 changes: 132 additions & 23 deletions components/dashboard/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ import { SSOLoginForm } from "./login/SSOLoginForm";
import { useAuthProviderDescriptions } from "./data/auth-providers/auth-provider-descriptions-query";
import { SetupPending } from "./login/SetupPending";
import { useNeedsSetup } from "./dedicated-setup/use-needs-setup";
import { useInstallationConfiguration } from "./data/installation/installation-config-query";
import { AuthProviderDescription } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import { Button, ButtonProps } from "@podkit/buttons/Button";
import { cn } from "@podkit/lib/cn";
import { userClient } from "./service/public-api";
import { ProductLogo } from "./components/ProductLogo";
import { useIsDataOps } from "./data/featureflag-query";
import GitpodClassicCard from "./images/gitpod-classic-card.png";
import { LoadingState } from "@podkit/loading/LoadingState";
import { isGitpodIo } from "./utils";
import { trackEvent } from "./Analytics";
import { useToast } from "./components/toasts/Toasts";
import onaWordmark from "./images/ona-wordmark.svg";
import onaApplication from "./images/ona-application.webp";

export function markLoggedIn() {
document.cookie = GitpodCookie.generateCookie(window.location.hostname);
Expand Down Expand Up @@ -64,7 +68,8 @@ export const Login: FC<LoginProps> = ({ onLoggedIn }) => {
const [hostFromContext, setHostFromContext] = useState<string | undefined>();
const [repoPathname, setRepoPathname] = useState<string | undefined>();

const enterprise = !!authProviders.data && authProviders.data.length === 0;
const { data: installationConfig } = useInstallationConfiguration();
const enterprise = !!installationConfig?.isDedicatedInstallation;

useEffect(() => {
try {
Expand Down Expand Up @@ -93,9 +98,15 @@ export const Login: FC<LoginProps> = ({ onLoggedIn }) => {
return (
<div
id="login-container"
className={cn("z-50 flex flex-col-reverse lg:flex-row w-full min-h-screen", {
"bg-[#FDF1E7] dark:bg-[#23211e]": !enterprise,
})}
className="z-50 flex flex-col-reverse lg:flex-row w-full min-h-screen"
style={
!enterprise
? {
background:
"linear-gradient(390deg, #1F1329 0%, #333A75 20%, #556CA8 50%, #90A898 60%, #90A898 70%, #E2B15C 90%, #BEA462 100%)",
}
: undefined
}
>
{enterprise ? (
<EnterpriseLoginWrapper
Expand Down Expand Up @@ -147,7 +158,7 @@ const PAYGLoginWrapper: FC<LoginWrapperProps> = ({ providerFromContext, repoPath
<div
id="login-section"
// for some reason, min-h-dvh does not work, so we need tailwind's arbitrary values
className="w-full min-h-[100dvh] lg:w-2/3 flex flex-col justify-center items-center bg-[#FDF1E7] dark:bg-[#23211e] p-2"
className="w-full min-h-[100dvh] lg:w-full flex flex-col justify-center items-center p-2"
>
<div
id="login-section-column"
Expand Down Expand Up @@ -212,6 +223,9 @@ const LoginContent = ({
const authProviders = useAuthProviderDescriptions();
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);

const { data: installationConfig } = useInstallationConfiguration();
const enterprise = !!installationConfig?.isDedicatedInstallation;

const updateUser = useCallback(async () => {
await getGitpodService().reconnect();
const { user } = await userClient.getAuthenticatedUser({});
Expand Down Expand Up @@ -314,32 +328,127 @@ const LoginContent = ({
<SSOLoginForm onSuccess={authorizeSuccessful} />
</div>
{errorMessage && <ErrorMessage imgSrc={exclamation} message={errorMessage} />}

{/* Gitpod Classic sunset notice - only show for non-enterprise */}
{!enterprise && (
<div className="mt-6 text-center text-sm">
<p className="text-pk-content-primary">
Gitpod classic is sunsetting fall 2025.{" "}
<a
href="https://app.gitpod.io"
target="_blank"
rel="noopener noreferrer"
className="gp-link hover:text-gray-600"
>
Try the new Gitpod
</a>{" "}
now (hosted compute & SWE agents coming soon)
</p>
</div>
)}
</div>
);
};

const RightProductDescriptionPanel = () => {
return (
<div className="w-full lg:w-1/3 flex flex-col md:justify-center p-4 lg:p-10 lg:pb-2 md:min-h-screen">
const [email, setEmail] = useState("");
const [isSubmitted, setIsSubmitted] = useState(false);
const { toast } = useToast();

const handleEmailSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!email.trim()) return;

trackEvent("waitlist_joined", { email: email, feature: "Ona" });

setIsSubmitted(true);

toast(
<div>
<div className="justify-center md:justify-start mb-6 md:mb-8">
<h2 className="text-2xl font-medium mb-2 dark:text-white inline-flex items-center gap-x-2">
Gitpod Classic
<div className="font-medium">You're on the waitlist</div>
<div className="text-sm opacity-80">We'll reach out to you soon.</div>
</div>,
);
};

return (
<div className="w-full lg:max-w-lg 2xl:max-w-[600px] flex flex-col justify-center px-4 lg:px-4 md:min-h-screen my-auto">
<div className="rounded-lg flex flex-col gap-6 text-white h-full py-4 lg:py-6 max-w-lg mx-auto w-full">
<div className="relative bg-white/10 backdrop-blur-sm rounded-lg pt-4 px-4 -mt-2">
<div className="flex justify-center pt-4 mb-4">
<img src={onaWordmark} alt="ONA" className="w-36" draggable="false" />
</div>
<div className="relative overflow-hidden">
<img
src={onaApplication}
alt="Ona application preview"
className="w-full h-auto rounded-lg shadow-lg translate-y-8"
draggable="false"
/>
</div>
</div>

<div className="flex flex-col gap-4 flex-1">
<h2 className="text-white text-xl font-bold leading-tight text-center max-w-sm mx-auto">
Meet Ona - the privacy-first software engineering agent.
</h2>
<p className="text-pk-content-secondary mb-2">
Automated, standardized development environments hosted by us in Gitpod’s infrastructure. Users
who joined before October 1, 2024 on non-Enterprise plans are considered Gitpod Classic users.
</p>

<p className="text-pk-content-secondary mb-2">
Gitpod Classic is sunsetting fall 2025.{" "}
<a className="gp-link font-bold" href="https://app.gitpod.io" target="_blank" rel="noreferrer">
Try the new Gitpod
</a>{" "}
now (hosted compute coming soon).
</p>
<div className="space-y-3 mt-4">
<p className="text-white/70 text-base">
Delegate software tasks to Ona. It writes code, runs tests, and opens a pull request. Or
jump in to inspect output or pair program in your IDE.
</p>
<p className="text-white/70 text-base mt-2">
Ona runs inside your infrastructure (VPC), with full audit trails, zero data exposure, and
support for any LLM.
</p>
</div>

<div className="mt-4">
{!isSubmitted ? (
<form onSubmit={handleEmailSubmit} className="space-y-3">
<div className="flex gap-2">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your work email"
className="flex-1 px-4 py-2.5 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 text-sm"
required
/>
<button
type="submit"
className="bg-white text-gray-900 font-medium py-2.5 px-4 rounded-lg hover:bg-gray-100 transition-colors text-sm inline-flex items-center justify-center gap-2"
>
Request access
<span className="font-bold">→</span>
</button>
</div>
<p className="text-xs text-white/70">
By submitting this, you agree to our{" "}
<a
href="https://www.gitpod.io/privacy/"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white"
>
privacy policy
</a>
</p>
</form>
) : (
<a
href="https://www.gitpod.io/solutions/ai"
target="_blank"
rel="noopener noreferrer"
className="w-full bg-white/20 backdrop-blur-sm text-white font-medium py-2.5 px-4 rounded-lg hover:bg-white/30 transition-colors border border-white/20 inline-flex items-center justify-center gap-2 text-sm"
>
Learn more
<span className="font-bold">→</span>
</a>
)}
</div>
</div>
<img src={GitpodClassicCard} alt="Gitpod Classic" className="w-full" />
</div>
</div>
);
Expand Down
Loading
Loading