mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
feat(frontend): login page cta (#13337)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -49,9 +49,17 @@ vi.mock("#/utils/custom-toast-handlers", () => ({
|
||||
displayErrorToast: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock feature flags - we'll control the return value in each test
|
||||
const mockEnableProjUserJourney = vi.fn(() => true);
|
||||
vi.mock("#/utils/feature-flags", () => ({
|
||||
ENABLE_PROJ_USER_JOURNEY: () => mockEnableProjUserJourney(),
|
||||
}));
|
||||
|
||||
describe("LoginContent", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("location", { href: "" });
|
||||
// Reset mock to return true by default
|
||||
mockEnableProjUserJourney.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -274,6 +282,65 @@ describe("LoginContent", () => {
|
||||
expect(screen.getByTestId("terms-and-privacy-notice")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the enterprise LoginCTA component when appMode is saas and feature flag enabled", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<LoginContent
|
||||
githubAuthUrl="https://github.com/oauth/authorize"
|
||||
appMode="saas"
|
||||
providersConfigured={["github"]}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("login-cta")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display the enterprise LoginCTA component when appMode is oss even with feature flag enabled", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<LoginContent
|
||||
githubAuthUrl="https://github.com/oauth/authorize"
|
||||
appMode="oss"
|
||||
providersConfigured={["github"]}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("login-cta")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display the enterprise LoginCTA component when appMode is null", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<LoginContent
|
||||
githubAuthUrl="https://github.com/oauth/authorize"
|
||||
appMode={null}
|
||||
providersConfigured={["github"]}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("login-cta")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display the enterprise LoginCTA component when feature flag is disabled", () => {
|
||||
// Disable the feature flag
|
||||
mockEnableProjUserJourney.mockReturnValue(false);
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<LoginContent
|
||||
githubAuthUrl="https://github.com/oauth/authorize"
|
||||
appMode="saas"
|
||||
providersConfigured={["github"]}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("login-cta")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display invitation pending message when hasInvitation is true", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { LoginCTA } from "#/components/features/auth/login-cta";
|
||||
|
||||
// Mock useTracking hook
|
||||
const mockTrackSaasSelfhostedInquiry = vi.fn();
|
||||
vi.mock("#/hooks/use-tracking", () => ({
|
||||
useTracking: () => ({
|
||||
trackSaasSelfhostedInquiry: mockTrackSaasSelfhostedInquiry,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("LoginCTA", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render enterprise CTA with title and description", () => {
|
||||
render(<LoginCTA />);
|
||||
|
||||
expect(screen.getByTestId("login-cta")).toBeInTheDocument();
|
||||
expect(screen.getByText("CTA$ENTERPRISE")).toBeInTheDocument();
|
||||
expect(screen.getByText("CTA$ENTERPRISE_DEPLOY")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render all enterprise feature list items", () => {
|
||||
render(<LoginCTA />);
|
||||
|
||||
expect(screen.getByText("CTA$FEATURE_ON_PREMISES")).toBeInTheDocument();
|
||||
expect(screen.getByText("CTA$FEATURE_DATA_CONTROL")).toBeInTheDocument();
|
||||
expect(screen.getByText("CTA$FEATURE_COMPLIANCE")).toBeInTheDocument();
|
||||
expect(screen.getByText("CTA$FEATURE_SUPPORT")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render Learn More as a link with correct href and target", () => {
|
||||
render(<LoginCTA />);
|
||||
|
||||
const learnMoreLink = screen.getByRole("link", {
|
||||
name: "CTA$LEARN_MORE",
|
||||
});
|
||||
expect(learnMoreLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://openhands.dev/enterprise/",
|
||||
);
|
||||
expect(learnMoreLink).toHaveAttribute("target", "_blank");
|
||||
expect(learnMoreLink).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
it("should call trackSaasSelfhostedInquiry with location 'login_page' when Learn More is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<LoginCTA />);
|
||||
|
||||
const learnMoreLink = screen.getByRole("link", {
|
||||
name: "CTA$LEARN_MORE",
|
||||
});
|
||||
await user.click(learnMoreLink);
|
||||
|
||||
expect(mockTrackSaasSelfhostedInquiry).toHaveBeenCalledWith({
|
||||
location: "login_page",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -73,6 +73,11 @@ vi.mock("#/hooks/use-invitation", () => ({
|
||||
useInvitation: () => useInvitationMock(),
|
||||
}));
|
||||
|
||||
// Mock feature flags - enable by default for tests
|
||||
vi.mock("#/utils/feature-flags", () => ({
|
||||
ENABLE_PROJ_USER_JOURNEY: () => true,
|
||||
}));
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: LoginPage,
|
||||
|
||||
@@ -13,6 +13,9 @@ import { TermsAndPrivacyNotice } from "#/components/shared/terms-and-privacy-not
|
||||
import { useRecaptcha } from "#/hooks/use-recaptcha";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ENABLE_PROJ_USER_JOURNEY } from "#/utils/feature-flags";
|
||||
import { LoginCTA } from "./login-cta";
|
||||
|
||||
export interface LoginContentProps {
|
||||
githubAuthUrl: string | null;
|
||||
@@ -177,125 +180,133 @@ export function LoginContent({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center w-full gap-12.5"
|
||||
data-testid="login-content"
|
||||
className={cn(
|
||||
"flex flex-col md:flex-row items-center md:items-stretch gap-6 h-full",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<OpenHandsLogoWhite width={106} height={72} />
|
||||
</div>
|
||||
<div
|
||||
className={cn("flex flex-col items-center w-full gap-12.5")}
|
||||
data-testid="login-content"
|
||||
>
|
||||
<div>
|
||||
<OpenHandsLogoWhite width={106} height={72} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-[39px] leading-5 font-medium text-white text-center">
|
||||
{t(I18nKey.AUTH$LETS_GET_STARTED)}
|
||||
</h1>
|
||||
<h1 className="text-[39px] leading-5 font-medium text-white text-center">
|
||||
{t(I18nKey.AUTH$LETS_GET_STARTED)}
|
||||
</h1>
|
||||
|
||||
{shouldShownHelperText && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{emailVerified && (
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
{t(I18nKey.AUTH$EMAIL_VERIFIED_PLEASE_LOGIN)}
|
||||
</p>
|
||||
)}
|
||||
{hasDuplicatedEmail && (
|
||||
<p className="text-sm text-danger text-center">
|
||||
{t(I18nKey.AUTH$DUPLICATE_EMAIL_ERROR)}
|
||||
</p>
|
||||
)}
|
||||
{recaptchaBlocked && (
|
||||
<p className="text-sm text-danger text-center max-w-125">
|
||||
{t(I18nKey.AUTH$RECAPTCHA_BLOCKED)}
|
||||
</p>
|
||||
)}
|
||||
{hasInvitation && (
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
{t(I18nKey.AUTH$INVITATION_PENDING)}
|
||||
</p>
|
||||
)}
|
||||
{showBitbucket && (
|
||||
<p className="text-sm text-white text-center max-w-125">
|
||||
{t(I18nKey.AUTH$BITBUCKET_SIGNUP_DISABLED)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShownHelperText && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{emailVerified && (
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
{t(I18nKey.AUTH$EMAIL_VERIFIED_PLEASE_LOGIN)}
|
||||
</p>
|
||||
)}
|
||||
{hasDuplicatedEmail && (
|
||||
<p className="text-sm text-danger text-center">
|
||||
{t(I18nKey.AUTH$DUPLICATE_EMAIL_ERROR)}
|
||||
</p>
|
||||
)}
|
||||
{recaptchaBlocked && (
|
||||
<p className="text-sm text-danger text-center max-w-125">
|
||||
{t(I18nKey.AUTH$RECAPTCHA_BLOCKED)}
|
||||
</p>
|
||||
)}
|
||||
{hasInvitation && (
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
{t(I18nKey.AUTH$INVITATION_PENDING)}
|
||||
</p>
|
||||
)}
|
||||
{showBitbucket && (
|
||||
<p className="text-sm text-white text-center max-w-125">
|
||||
{t(I18nKey.AUTH$BITBUCKET_SIGNUP_DISABLED)}
|
||||
</p>
|
||||
{noProvidersConfigured ? (
|
||||
<div className="text-center p-4 text-muted-foreground">
|
||||
{t(I18nKey.AUTH$NO_PROVIDERS_CONFIGURED)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{showGithub && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGitHubAuth}
|
||||
className={`${buttonBaseClasses} bg-[#9E28B0] text-white`}
|
||||
>
|
||||
<GitHubLogo width={14} height={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(I18nKey.GITHUB$CONNECT_TO_GITHUB)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showGitlab && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGitLabAuth}
|
||||
className={`${buttonBaseClasses} bg-[#FC6B0E] text-white`}
|
||||
>
|
||||
<GitLabLogo width={14} height={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(I18nKey.GITLAB$CONNECT_TO_GITLAB)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showBitbucket && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBitbucketAuth}
|
||||
className={`${buttonBaseClasses} bg-[#2684FF] text-white`}
|
||||
>
|
||||
<BitbucketLogo width={14} height={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(I18nKey.BITBUCKET$CONNECT_TO_BITBUCKET)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showBitbucketDataCenter && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBitbucketDataCenterAuth}
|
||||
className={`${buttonBaseClasses} bg-[#2684FF] text-white`}
|
||||
>
|
||||
<BitbucketLogo width={14} height={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(
|
||||
I18nKey.BITBUCKET_DATA_CENTER$CONNECT_TO_BITBUCKET_DATA_CENTER,
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showEnterpriseSso && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEnterpriseSsoAuth}
|
||||
className={`${buttonBaseClasses} bg-[#374151] text-white`}
|
||||
>
|
||||
<FaUserShield size={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(I18nKey.ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{noProvidersConfigured ? (
|
||||
<div className="text-center p-4 text-muted-foreground">
|
||||
{t(I18nKey.AUTH$NO_PROVIDERS_CONFIGURED)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{showGithub && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGitHubAuth}
|
||||
className={`${buttonBaseClasses} bg-[#9E28B0] text-white`}
|
||||
>
|
||||
<GitHubLogo width={14} height={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(I18nKey.GITHUB$CONNECT_TO_GITHUB)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showGitlab && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGitLabAuth}
|
||||
className={`${buttonBaseClasses} bg-[#FC6B0E] text-white`}
|
||||
>
|
||||
<GitLabLogo width={14} height={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(I18nKey.GITLAB$CONNECT_TO_GITLAB)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showBitbucket && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBitbucketAuth}
|
||||
className={`${buttonBaseClasses} bg-[#2684FF] text-white`}
|
||||
>
|
||||
<BitbucketLogo width={14} height={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(I18nKey.BITBUCKET$CONNECT_TO_BITBUCKET)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showBitbucketDataCenter && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBitbucketDataCenterAuth}
|
||||
className={`${buttonBaseClasses} bg-[#2684FF] text-white`}
|
||||
>
|
||||
<BitbucketLogo width={14} height={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(
|
||||
I18nKey.BITBUCKET_DATA_CENTER$CONNECT_TO_BITBUCKET_DATA_CENTER,
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showEnterpriseSso && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEnterpriseSsoAuth}
|
||||
className={`${buttonBaseClasses} bg-[#374151] text-white`}
|
||||
>
|
||||
<FaUserShield size={14} className="shrink-0" />
|
||||
<span className={buttonLabelClasses}>
|
||||
{t(I18nKey.ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO)}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<TermsAndPrivacyNotice className="max-w-[320px] text-[#A3A3A3]" />
|
||||
</div>
|
||||
|
||||
<TermsAndPrivacyNotice className="max-w-[320px] text-[#A3A3A3]" />
|
||||
{appMode === "saas" && ENABLE_PROJ_USER_JOURNEY() && <LoginCTA />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
66
frontend/src/components/features/auth/login-cta.tsx
Normal file
66
frontend/src/components/features/auth/login-cta.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card } from "#/ui/card";
|
||||
import { CardTitle } from "#/ui/card-title";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { cn } from "#/utils/utils";
|
||||
import StackedIcon from "#/icons/stacked.svg?react";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
export function LoginCTA() {
|
||||
const { t } = useTranslation();
|
||||
const { trackSaasSelfhostedInquiry } = useTracking();
|
||||
|
||||
const handleLearnMoreClick = () => {
|
||||
trackSaasSelfhostedInquiry({ location: "login_page" });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
testId="login-cta"
|
||||
theme="dark"
|
||||
className={cn("w-full max-w-80 h-auto flex-col", "cta-card-gradient")}
|
||||
>
|
||||
<div className={cn("flex flex-col gap-[11px] p-6")}>
|
||||
<div className={cn("size-10")}>
|
||||
<StackedIcon width={40} height={40} />
|
||||
</div>
|
||||
|
||||
<CardTitle>{t(I18nKey.CTA$ENTERPRISE)}</CardTitle>
|
||||
|
||||
<Typography.Text className="text-[#8C8C8C] font-inter font-normal text-sm leading-5">
|
||||
{t(I18nKey.CTA$ENTERPRISE_DEPLOY)}
|
||||
</Typography.Text>
|
||||
|
||||
<ul
|
||||
className={cn(
|
||||
"text-[#8C8C8C] font-inter font-normal text-sm leading-5 list-disc list-inside flex flex-col gap-1",
|
||||
)}
|
||||
>
|
||||
<li>{t(I18nKey.CTA$FEATURE_ON_PREMISES)}</li>
|
||||
<li>{t(I18nKey.CTA$FEATURE_DATA_CONTROL)}</li>
|
||||
<li>{t(I18nKey.CTA$FEATURE_COMPLIANCE)}</li>
|
||||
<li>{t(I18nKey.CTA$FEATURE_SUPPORT)}</li>
|
||||
</ul>
|
||||
|
||||
<div className={cn("h-10 flex justify-start")}>
|
||||
<a
|
||||
href="https://openhands.dev/enterprise/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleLearnMoreClick}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center",
|
||||
"h-10 px-4 rounded",
|
||||
"bg-[#050505] border border-[#242424]",
|
||||
"text-white hover:bg-[#0a0a0a]",
|
||||
"font-semibold text-sm",
|
||||
)}
|
||||
>
|
||||
{t(I18nKey.CTA$LEARN_MORE)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1141,6 +1141,12 @@ export enum I18nKey {
|
||||
ONBOARDING$NEXT_BUTTON = "ONBOARDING$NEXT_BUTTON",
|
||||
ONBOARDING$BACK_BUTTON = "ONBOARDING$BACK_BUTTON",
|
||||
ONBOARDING$FINISH_BUTTON = "ONBOARDING$FINISH_BUTTON",
|
||||
CTA$ENTERPRISE = "CTA$ENTERPRISE",
|
||||
CTA$ENTERPRISE_DEPLOY = "CTA$ENTERPRISE_DEPLOY",
|
||||
CTA$FEATURE_ON_PREMISES = "CTA$FEATURE_ON_PREMISES",
|
||||
CTA$FEATURE_DATA_CONTROL = "CTA$FEATURE_DATA_CONTROL",
|
||||
CTA$FEATURE_COMPLIANCE = "CTA$FEATURE_COMPLIANCE",
|
||||
CTA$FEATURE_SUPPORT = "CTA$FEATURE_SUPPORT",
|
||||
ENTERPRISE$SELF_HOSTED = "ENTERPRISE$SELF_HOSTED",
|
||||
ENTERPRISE$TITLE = "ENTERPRISE$TITLE",
|
||||
ENTERPRISE$DESCRIPTION = "ENTERPRISE$DESCRIPTION",
|
||||
|
||||
@@ -18255,6 +18255,102 @@
|
||||
"tr": "Bitir",
|
||||
"uk": "Завершити"
|
||||
},
|
||||
"CTA$ENTERPRISE": {
|
||||
"en": "Enterprise",
|
||||
"ja": "エンタープライズ",
|
||||
"zh-CN": "企业版",
|
||||
"zh-TW": "企業版",
|
||||
"ko-KR": "엔터프라이즈",
|
||||
"no": "Bedrift",
|
||||
"ar": "المؤسسات",
|
||||
"de": "Unternehmen",
|
||||
"fr": "Entreprise",
|
||||
"it": "Azienda",
|
||||
"pt": "Empresarial",
|
||||
"es": "Empresa",
|
||||
"tr": "Kurumsal",
|
||||
"uk": "Підприємство"
|
||||
},
|
||||
"CTA$ENTERPRISE_DEPLOY": {
|
||||
"en": "Deploy OpenHands on your own infrastructure. Full control over data, compliance, and security.",
|
||||
"ja": "独自のインフラストラクチャにOpenHandsをデプロイ。データ、コンプライアンス、セキュリティを完全にコントロール。",
|
||||
"zh-CN": "在您自己的基础设施上部署 OpenHands。完全控制数据、合规性和安全性。",
|
||||
"zh-TW": "在您自己的基礎設施上部署 OpenHands。完全控制資料、合規性和安全性。",
|
||||
"ko-KR": "자체 인프라에 OpenHands를 배포하세요. 데이터, 규정 준수 및 보안을 완벽하게 제어합니다.",
|
||||
"no": "Distribuer OpenHands på din egen infrastruktur. Full kontroll over data, samsvar og sikkerhet.",
|
||||
"ar": "انشر OpenHands على بنيتك التحتية الخاصة. تحكم كامل في البيانات والامتثال والأمان.",
|
||||
"de": "Stellen Sie OpenHands auf Ihrer eigenen Infrastruktur bereit. Volle Kontrolle über Daten, Compliance und Sicherheit.",
|
||||
"fr": "Déployez OpenHands sur votre propre infrastructure. Contrôle total sur les données, la conformité et la sécurité.",
|
||||
"it": "Distribuisci OpenHands sulla tua infrastruttura. Controllo completo su dati, conformità e sicurezza.",
|
||||
"pt": "Implante o OpenHands em sua própria infraestrutura. Controle total sobre dados, conformidade e segurança.",
|
||||
"es": "Implemente OpenHands en su propia infraestructura. Control total sobre datos, cumplimiento y seguridad.",
|
||||
"tr": "OpenHands'i kendi altyapınızda dağıtın. Veri, uyumluluk ve güvenlik üzerinde tam kontrol.",
|
||||
"uk": "Розгорніть OpenHands на власній інфраструктурі. Повний контроль над даними, відповідністю та безпекою."
|
||||
},
|
||||
"CTA$FEATURE_ON_PREMISES": {
|
||||
"en": "On-premises or private cloud",
|
||||
"ja": "オンプレミスまたはプライベートクラウド",
|
||||
"zh-CN": "本地部署或私有云",
|
||||
"zh-TW": "本地部署或私有雲",
|
||||
"ko-KR": "온프레미스 또는 프라이빗 클라우드",
|
||||
"no": "Lokalt eller privat sky",
|
||||
"ar": "محلي أو سحابة خاصة",
|
||||
"de": "On-Premises oder Private Cloud",
|
||||
"fr": "Sur site ou cloud privé",
|
||||
"it": "On-premise o cloud privato",
|
||||
"pt": "Local ou nuvem privada",
|
||||
"es": "Local o nube privada",
|
||||
"tr": "Şirket içi veya özel bulut",
|
||||
"uk": "Локально або приватна хмара"
|
||||
},
|
||||
"CTA$FEATURE_DATA_CONTROL": {
|
||||
"en": "Full data control",
|
||||
"ja": "完全なデータ管理",
|
||||
"zh-CN": "完全数据控制",
|
||||
"zh-TW": "完全資料控制",
|
||||
"ko-KR": "완전한 데이터 제어",
|
||||
"no": "Full datakontroll",
|
||||
"ar": "تحكم كامل في البيانات",
|
||||
"de": "Volle Datenkontrolle",
|
||||
"fr": "Contrôle total des données",
|
||||
"it": "Controllo completo dei dati",
|
||||
"pt": "Controle total de dados",
|
||||
"es": "Control total de datos",
|
||||
"tr": "Tam veri kontrolü",
|
||||
"uk": "Повний контроль даних"
|
||||
},
|
||||
"CTA$FEATURE_COMPLIANCE": {
|
||||
"en": "Custom compliance requirements",
|
||||
"ja": "カスタムコンプライアンス要件",
|
||||
"zh-CN": "自定义合规要求",
|
||||
"zh-TW": "自訂合規要求",
|
||||
"ko-KR": "사용자 정의 규정 준수 요구 사항",
|
||||
"no": "Tilpassede samsvarkrav",
|
||||
"ar": "متطلبات الامتثال المخصصة",
|
||||
"de": "Individuelle Compliance-Anforderungen",
|
||||
"fr": "Exigences de conformité personnalisées",
|
||||
"it": "Requisiti di conformità personalizzati",
|
||||
"pt": "Requisitos de conformidade personalizados",
|
||||
"es": "Requisitos de cumplimiento personalizados",
|
||||
"tr": "Özel uyumluluk gereksinimleri",
|
||||
"uk": "Індивідуальні вимоги відповідності"
|
||||
},
|
||||
"CTA$FEATURE_SUPPORT": {
|
||||
"en": "Dedicated support options",
|
||||
"ja": "専用サポートオプション",
|
||||
"zh-CN": "专属支持选项",
|
||||
"zh-TW": "專屬支援選項",
|
||||
"ko-KR": "전용 지원 옵션",
|
||||
"no": "Dedikerte støttealternativer",
|
||||
"ar": "خيارات دعم مخصصة",
|
||||
"de": "Dedizierte Supportoptionen",
|
||||
"fr": "Options de support dédiées",
|
||||
"it": "Opzioni di supporto dedicate",
|
||||
"pt": "Opções de suporte dedicado",
|
||||
"es": "Opciones de soporte dedicado",
|
||||
"tr": "Özel destek seçenekleri",
|
||||
"uk": "Виділені варіанти підтримки"
|
||||
},
|
||||
"ENTERPRISE$SELF_HOSTED": {
|
||||
"en": "Self-Hosted",
|
||||
"ja": "セルフホスト",
|
||||
|
||||
@@ -379,6 +379,6 @@
|
||||
|
||||
/* CTA card gradient and shadow */
|
||||
.cta-card-gradient {
|
||||
background: radial-gradient(85.36% 123.38% at 50% 0%, rgba(255, 255, 255, 0.14) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
background: radial-gradient(85.36% 123.38% at 50% 0%, rgba(255, 255, 255, 0.14) 0%, rgba(0, 0, 0, 0) 100%), #000000;
|
||||
box-shadow: 0px 4px 6px -4px rgba(0, 0, 0, 0.1), 0px 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user