diff --git a/components/dashboard/src/AppNotifications.tsx b/components/dashboard/src/AppNotifications.tsx index 5561b6b474e51c..14783a2b21340b 100644 --- a/components/dashboard/src/AppNotifications.tsx +++ b/components/dashboard/src/AppNotifications.tsx @@ -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"; @@ -129,29 +131,76 @@ 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: ( - - Meet Ona | the - privacy-first software engineering agent |{" "} - - Get early access - - - ), -} 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: ( + + Meet Ona | + the privacy-first software engineering agent |{" "} + {!onaClicked ? ( + + ) : ( + + Learn more + + )} + + ), + } as Notification; +}; export function AppNotifications() { const [topNotification, setTopNotification] = useState(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( +
+
You're on the waitlist
+
We'll reach out to you soon.
+
, + ); + }, [user, toast]); + useEffect(() => { let ignore = false; @@ -159,7 +208,7 @@ export function AppNotifications() { const notifications = []; if (!loading) { if (isGitpodIo()) { - notifications.push(GITPOD_CLASSIC_SUNSET); + notifications.push(GITPOD_CLASSIC_SUNSET(user, toast, onaClicked, handleOnaBannerClick)); } if ( @@ -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) { @@ -213,7 +262,7 @@ export function AppNotifications() { {topNotification && ( { if (!topNotification.preventDismiss) { dismissNotification(); diff --git a/components/dashboard/src/Login.tsx b/components/dashboard/src/Login.tsx index 5bf6fe3fc22824..d9ca1cda32b909 100644 --- a/components/dashboard/src/Login.tsx +++ b/components/dashboard/src/Login.tsx @@ -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); @@ -64,7 +68,8 @@ export const Login: FC = ({ onLoggedIn }) => { const [hostFromContext, setHostFromContext] = useState(); const [repoPathname, setRepoPathname] = useState(); - const enterprise = !!authProviders.data && authProviders.data.length === 0; + const { data: installationConfig } = useInstallationConfiguration(); + const enterprise = !!installationConfig?.isDedicatedInstallation; useEffect(() => { try { @@ -93,9 +98,15 @@ export const Login: FC = ({ onLoggedIn }) => { return (
{enterprise ? ( = ({ providerFromContext, repoPath
(undefined); + const { data: installationConfig } = useInstallationConfiguration(); + const enterprise = !!installationConfig?.isDedicatedInstallation; + const updateUser = useCallback(async () => { await getGitpodService().reconnect(); const { user } = await userClient.getAuthenticatedUser({}); @@ -314,32 +328,127 @@ const LoginContent = ({
{errorMessage && } + + {/* Gitpod Classic sunset notice - only show for non-enterprise */} + {!enterprise && ( +
+

+ Gitpod classic is sunsetting fall 2025.{" "} + + Try the new Gitpod + {" "} + now (hosted compute & SWE agents coming soon) +

+
+ )}
); }; const RightProductDescriptionPanel = () => { - return ( -
+ 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(
-
-

- Gitpod Classic +
You're on the waitlist
+
We'll reach out to you soon.
+

, + ); + }; + + return ( +
+
+
+
+ ONA +
+
+ Ona application preview +
+
+ +
+

+ Meet Ona - the privacy-first software engineering agent.

-

- 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. -

-

- Gitpod Classic is sunsetting fall 2025.{" "} - - Try the new Gitpod - {" "} - now (hosted compute coming soon). -

+
+

+ 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. +

+

+ Ona runs inside your infrastructure (VPC), with full audit trails, zero data exposure, and + support for any LLM. +

+
+ +
+ {!isSubmitted ? ( +
+
+ 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 + /> + +
+

+ By submitting this, you agree to our{" "} + + privacy policy + +

+
+ ) : ( + + Learn more + + + )} +
- Gitpod Classic
); diff --git a/components/dashboard/src/OnaRightPanel.tsx b/components/dashboard/src/OnaRightPanel.tsx new file mode 100644 index 00000000000000..dab61ac0eb0958 --- /dev/null +++ b/components/dashboard/src/OnaRightPanel.tsx @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2025 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, { useState, useEffect } from "react"; +import { trackEvent } from "./Analytics"; +import { useCurrentUser } from "./user-context"; +import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils"; +import { useToast } from "./components/toasts/Toasts"; +import onaWordmark from "./images/ona-wordmark.svg"; +import onaApplication from "./images/ona-application.webp"; + +export const OnaRightPanel = () => { + const [email, setEmail] = useState(""); + const [isSubmitted, setIsSubmitted] = useState(false); + const user = useCurrentUser(); + const { toast } = useToast(); + + useEffect(() => { + const storedOnaData = localStorage.getItem("ona-waitlist-data"); + if (storedOnaData) { + const { submitted } = JSON.parse(storedOnaData); + setIsSubmitted(submitted || false); + } + }, []); + + const handleEmailSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!email.trim()) return; + + const userEmail = user ? getPrimaryEmail(user) || email : email; + trackEvent("waitlist_joined", { email: userEmail, feature: "Ona" }); + + setIsSubmitted(true); + localStorage.setItem("ona-waitlist-data", JSON.stringify({ submitted: true })); + + toast( +
+
You're on the waitlist
+
We'll reach out to you soon.
+
, + ); + }; + + return ( +
+
+
+ ONA +
+ +
+ Ona application preview +
+ +
+

+ Meet Ona - the privacy-first software engineering agent. +

+ +
+

+ 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. +

+

+ Ona runs inside your infrastructure (VPC), with full audit trails, zero data exposure, and + support for any LLM. +

+
+ +
+ {!isSubmitted ? ( +
+ setEmail(e.target.value)} + placeholder="Enter your work email" + className="w-full 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 + /> + +
+ ) : ( + + )} +
+
+
+
+ ); +}; diff --git a/components/dashboard/src/images/ona-application.webp b/components/dashboard/src/images/ona-application.webp new file mode 100644 index 00000000000000..a2ab9e8f6a9a6a Binary files /dev/null and b/components/dashboard/src/images/ona-application.webp differ diff --git a/components/dashboard/src/start/OnaBanner.tsx b/components/dashboard/src/start/OnaBanner.tsx new file mode 100644 index 00000000000000..d4c0a373a63791 --- /dev/null +++ b/components/dashboard/src/start/OnaBanner.tsx @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2024 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, { useEffect, useState } from "react"; +import { trackEvent } from "../Analytics"; +import { useCurrentUser } from "../user-context"; +import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils"; +import { useToast } from "../components/toasts/Toasts"; +import onaWordmark from "../images/ona-wordmark.svg"; + +const onaBanner = { + type: "Introducing", + title: "ONA", + subtitle: "The privacy-first software engineering agent.", + ctaText: "Get early access", + learnMoreText: "Learn more", + link: "https://www.gitpod.io/solutions/ai", +}; + +interface OnaBannerProps { + compact?: boolean; +} + +export const OnaBanner: React.FC = ({ compact = false }) => { + const [onaClicked, setOnaClicked] = useState(false); + const [isDismissed, setIsDismissed] = useState(false); + const user = useCurrentUser(); + const { toast } = useToast(); + + useEffect(() => { + const storedOnaData = localStorage.getItem("ona-banner-data"); + + if (storedOnaData) { + const { clicked, dismissed } = JSON.parse(storedOnaData); + setOnaClicked(clicked || false); + setIsDismissed(dismissed || false); + } + }, []); + + const handleOnaBannerClick = () => { + // Track "Get early access" click + const userEmail = user ? getPrimaryEmail(user) || "" : ""; + trackEvent("waitlist_joined", { email: userEmail, feature: "Ona" }); + + setOnaClicked(true); + localStorage.setItem("ona-banner-data", JSON.stringify({ clicked: true, dismissed: isDismissed })); + + // Show success toast + toast( +
+
You're on the waitlist
+
We'll reach out to you soon.
+
, + ); + }; + + const handleDismiss = () => { + setIsDismissed(true); + localStorage.setItem("ona-banner-data", JSON.stringify({ clicked: onaClicked, dismissed: true })); + }; + + // Don't render if dismissed + if (isDismissed) { + return null; + } + + if (compact) { + return ( +
+ {/* Close button */} + + + {/* Compact layout */} +
+ {onaBanner.type} + ONA +
+ +

+ The privacy-first software engineering agent +

+ + {!onaClicked ? ( + + ) : ( + + {onaBanner.learnMoreText} + + + )} +
+ ); + } + + return ( +
+ {/* Close button */} + + + {/* Left section - ONA branding and image */} +
+
+ {/* ONA Logo prominently displayed */} +
+ ONA +
+
+
+ + {/* Right section - Text content and CTA */} +
+
+ {/* Main title */} +

+ The privacy-first software engineering agent +

+ + {/* CTA Button */} +
+ {!onaClicked ? ( + + ) : ( + + {onaBanner.learnMoreText} + + + )} +
+
+
+
+ ); +}; diff --git a/components/dashboard/src/start/StartPage.tsx b/components/dashboard/src/start/StartPage.tsx index 5fcc96d4c9d877..309dc99d924385 100644 --- a/components/dashboard/src/start/StartPage.tsx +++ b/components/dashboard/src/start/StartPage.tsx @@ -15,6 +15,8 @@ import { useWorkspaceDefaultImageQuery } from "../data/workspaces/default-worksp import { GetWorkspaceDefaultImageResponse_Source } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb"; import { ProductLogo } from "../components/ProductLogo"; import { useIsDataOps } from "../data/featureflag-query"; +import { isGitpodIo } from "../utils"; +import { OnaBanner } from "./OnaBanner"; export enum StartPhase { Checking = 0, @@ -100,7 +102,14 @@ export function StartPage(props: StartPageProps) { const isDataOps = useIsDataOps(); return ( -
+
+ {/* OnaBanner positioned on the side when workspace is running */} + {isGitpodIo() && ( +
+ +
+ )} +
{ - const [showOnaBanner, setShowOnaBanner] = useState(true); const [onaClicked, setOnaClicked] = useState(false); + const [isDismissed, setIsDismissed] = useState(false); const user = useCurrentUser(); const { toast } = useToast(); useEffect(() => { - const storedOnaData = localStorage.getItem("ona-banner-data"); + const storedOnaData = localStorage.getItem("workspaces-ona-banner-data"); // Check Ona banner state if (storedOnaData) { - const { dismissed, clicked } = JSON.parse(storedOnaData); - setShowOnaBanner(!dismissed); + const { clicked, dismissed } = JSON.parse(storedOnaData); setOnaClicked(clicked || false); + setIsDismissed(dismissed || false); } // Clean up old blog banner data @@ -47,7 +47,15 @@ export const OnaBanner: React.FC = () => { trackEvent("waitlist_joined", { email: userEmail, feature: "Ona" }); setOnaClicked(true); - localStorage.setItem("ona-banner-data", JSON.stringify({ dismissed: false, clicked: true })); + localStorage.setItem( + "workspaces-ona-banner-data", + JSON.stringify({ clicked: true, dismissed: isDismissed }), + ); + + // Also set the global ona-banner-data clicked state (preserve existing dismissed state) + const existingOnaData = localStorage.getItem("ona-banner-data"); + const existingDismissed = existingOnaData ? JSON.parse(existingOnaData).dismissed || false : false; + localStorage.setItem("ona-banner-data", JSON.stringify({ clicked: true, dismissed: existingDismissed })); // Show success toast toast( @@ -62,48 +70,51 @@ export const OnaBanner: React.FC = () => { } }; - const handleOnaBannerDismiss = () => { - setShowOnaBanner(false); - localStorage.setItem("ona-banner-data", JSON.stringify({ dismissed: true, clicked: onaClicked })); + const handleDismiss = () => { + setIsDismissed(true); + localStorage.setItem("workspaces-ona-banner-data", JSON.stringify({ clicked: onaClicked, dismissed: true })); }; + // Don't render if dismissed + if (isDismissed) { + return null; + } + return (
- {showOnaBanner && ( -
+ {/* Close button */} + - - {/* Content */} -
-
- {onaBanner.type} - ONA -
-
{onaBanner.subtitle}
-
+ ✕ + - {/* CTA Button */} - + {/* Content */} +
+
+ {onaBanner.type} + ONA +
+
{onaBanner.subtitle}
- )} + + {/* CTA Button */} + +
); }; diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index ec6703814770ca..94bc7508a41f6b 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -34,7 +34,7 @@ import { useUserLoader } from "../hooks/use-user-loader"; import { ReactComponent as GitpodStrokedSVG } from "../icons/gitpod-stroked.svg"; import { VideoSection } from "../onboarding/VideoSection"; import { OrganizationJoinModal } from "../teams/onboarding/OrganizationJoinModal"; -import { BlogBanners } from "./BlogBanners"; +// import { BlogBanners } from "./BlogBanners"; import { EmptyWorkspacesContent } from "./EmptyWorkspacesContent"; import PersonalizedContent from "./PersonalizedContent"; import { VideoCarousel } from "./VideoCarousel"; @@ -504,7 +504,8 @@ const WorkspacesPage: FunctionComponent = () => {
- + {/* Uncomment the following, if you need side banners in future */} + {/* */}
)}