From fc924f707c6b9251fed3c51778511b695b038c79 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 18 Dec 2025 17:47:54 -0500 Subject: [PATCH] add banners --- messages/en-US.json | 14 +++ .../[orgId]/settings/clients/machine/page.tsx | 3 + .../settings/resources/client/page.tsx | 3 + .../[orgId]/settings/resources/proxy/page.tsx | 3 + src/app/[orgId]/settings/sites/page.tsx | 3 + src/components/ClientDownloadBanner.tsx | 69 +++++++++++++ src/components/DismissableBanner.tsx | 98 +++++++++++++++++++ src/components/MachineClientsBanner.tsx | 60 ++++++++++++ src/components/PrivateResourcesBanner.tsx | 54 ++++++++++ src/components/ProxyResourcesBanner.tsx | 23 +++++ src/components/SitesBanner.tsx | 40 ++++++++ src/components/UserDevicesTable.tsx | 3 + 12 files changed, 373 insertions(+) create mode 100644 src/components/ClientDownloadBanner.tsx create mode 100644 src/components/DismissableBanner.tsx create mode 100644 src/components/MachineClientsBanner.tsx create mode 100644 src/components/PrivateResourcesBanner.tsx create mode 100644 src/components/ProxyResourcesBanner.tsx create mode 100644 src/components/SitesBanner.tsx diff --git a/messages/en-US.json b/messages/en-US.json index b71eb202..e0728c94 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -51,6 +51,9 @@ "siteQuestionRemove": "Are you sure you want to remove the site from the organization?", "siteManageSites": "Manage Sites", "siteDescription": "Create and manage sites to enable connectivity to private networks", + "sitesBannerTitle": "Connect Any Network", + "sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.", + "sitesBannerButtonText": "Install Site", "siteCreate": "Create Site", "siteCreateDescription2": "Follow the steps below to create and connect a new site", "siteCreateDescription": "Create a new site to start connecting resources", @@ -147,8 +150,12 @@ "shareErrorSelectResource": "Please select a resource", "proxyResourceTitle": "Manage Public Resources", "proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser", + "proxyResourcesBannerTitle": "Web-based Public Access", + "proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.", "clientResourceTitle": "Manage Private Resources", "clientResourceDescription": "Create and manage resources that are only accessible through a connected client", + "privateResourcesBannerTitle": "Zero-Trust Private Access", + "privateResourcesBannerDescription": "Private resources use zero-trust security, ensuring users and machines can only access resources you explicitly grant. Connect user devices or machine clients to access these resources over a secure virtual private network.", "resourcesSearch": "Search resources...", "resourceAdd": "Add Resource", "resourceErrorDelte": "Error deleting resource", @@ -1944,8 +1951,15 @@ "beta": "Beta", "manageUserDevices": "User Devices", "manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources", + "downloadClientBannerTitle": "Download Pangolin Client", + "downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately", "manageMachineClients": "Manage Machine Clients", "manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources", + "machineClientsBannerTitle": "Servers & Automated Systems", + "machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.", + "machineClientsBannerPangolinCLI": "Pangolin CLI", + "machineClientsBannerOlmCLI": "Olm CLI", + "machineClientsBannerOlmContainer": "Olm Container", "clientsTableUserClients": "User", "clientsTableMachineClients": "Machine", "licenseTableValidUntil": "Valid Until", diff --git a/src/app/[orgId]/settings/clients/machine/page.tsx b/src/app/[orgId]/settings/clients/machine/page.tsx index e1a904ad..f2618bc2 100644 --- a/src/app/[orgId]/settings/clients/machine/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/page.tsx @@ -1,6 +1,7 @@ import type { ClientRow } from "@app/components/MachineClientsTable"; import MachineClientsTable from "@app/components/MachineClientsTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import MachineClientsBanner from "@app/components/MachineClientsBanner"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListClientsResponse } from "@server/routers/client"; @@ -71,6 +72,8 @@ export default async function ClientsPage(props: ClientsPageProps) { description={t("manageMachineClientsDescription")} /> + + + + + + + + ); diff --git a/src/components/ClientDownloadBanner.tsx b/src/components/ClientDownloadBanner.tsx new file mode 100644 index 00000000..dcd572fd --- /dev/null +++ b/src/components/ClientDownloadBanner.tsx @@ -0,0 +1,69 @@ +"use client"; + +import React from "react"; +import { Button } from "@app/components/ui/button"; +import { Download } from "lucide-react"; +import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import DismissableBanner from "./DismissableBanner"; + +export const ClientDownloadBanner = () => { + const t = useTranslations(); + + return ( + } + description={t("downloadClientBannerDescription")} + > + + + + + + + + + + + ); +}; + +export default ClientDownloadBanner; + diff --git a/src/components/DismissableBanner.tsx b/src/components/DismissableBanner.tsx new file mode 100644 index 00000000..6f49e036 --- /dev/null +++ b/src/components/DismissableBanner.tsx @@ -0,0 +1,98 @@ +"use client"; + +import React, { useState, useEffect, type ReactNode } from "react"; +import { Card, CardContent } from "@app/components/ui/card"; +import { X } from "lucide-react"; +import { useTranslations } from "next-intl"; + +type DismissableBannerProps = { + storageKey: string; + version: number; + title: string; + titleIcon: ReactNode; + description: string; + children?: ReactNode; +}; + +export const DismissableBanner = ({ + storageKey, + version, + title, + titleIcon, + description, + children +}: DismissableBannerProps) => { + const [isDismissed, setIsDismissed] = useState(true); + const t = useTranslations(); + + useEffect(() => { + const dismissedData = localStorage.getItem(storageKey); + if (dismissedData) { + try { + const parsed = JSON.parse(dismissedData); + // If version matches, use the dismissed state + if (parsed.version === version) { + setIsDismissed(parsed.dismissed); + } else { + // Version changed, show the banner again + setIsDismissed(false); + } + } catch { + // If parsing fails, check for old format (just "true" string) + if (dismissedData === "true") { + // Old format, show banner again for new version + setIsDismissed(false); + } else { + setIsDismissed(true); + } + } + } else { + setIsDismissed(false); + } + }, [storageKey, version]); + + const handleDismiss = () => { + setIsDismissed(true); + localStorage.setItem( + storageKey, + JSON.stringify({ dismissed: true, version }) + ); + }; + + if (isDismissed) { + return null; + } + + return ( + + + +
+
+

+ {titleIcon} + {title} +

+

+ {description} +

+
+ {children && ( +
+ {children} +
+ )} +
+
+
+ ); +}; + +export default DismissableBanner; + diff --git a/src/components/MachineClientsBanner.tsx b/src/components/MachineClientsBanner.tsx new file mode 100644 index 00000000..f69fa061 --- /dev/null +++ b/src/components/MachineClientsBanner.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import { Button } from "@app/components/ui/button"; +import { Server, Terminal, Container } from "lucide-react"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import DismissableBanner from "./DismissableBanner"; + +type MachineClientsBannerProps = { + orgId: string; +}; + +export const MachineClientsBanner = ({ + orgId +}: MachineClientsBannerProps) => { + const t = useTranslations(); + + return ( + } + description={t("machineClientsBannerDescription")} + > + + + + + + + + ); +}; + +export default MachineClientsBanner; + diff --git a/src/components/PrivateResourcesBanner.tsx b/src/components/PrivateResourcesBanner.tsx new file mode 100644 index 00000000..8320178d --- /dev/null +++ b/src/components/PrivateResourcesBanner.tsx @@ -0,0 +1,54 @@ +"use client"; + +import React from "react"; +import { Button } from "@app/components/ui/button"; +import { Shield, ArrowRight, Laptop, Server } from "lucide-react"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import DismissableBanner from "./DismissableBanner"; + +type PrivateResourcesBannerProps = { + orgId: string; +}; + +export const PrivateResourcesBanner = ({ + orgId +}: PrivateResourcesBannerProps) => { + const t = useTranslations(); + + return ( + } + description={t("privateResourcesBannerDescription")} + > + + + + + + + + ); +}; + +export default PrivateResourcesBanner; + diff --git a/src/components/ProxyResourcesBanner.tsx b/src/components/ProxyResourcesBanner.tsx new file mode 100644 index 00000000..40616758 --- /dev/null +++ b/src/components/ProxyResourcesBanner.tsx @@ -0,0 +1,23 @@ +"use client"; + +import React from "react"; +import { Globe } from "lucide-react"; +import { useTranslations } from "next-intl"; +import DismissableBanner from "./DismissableBanner"; + +export const ProxyResourcesBanner = () => { + const t = useTranslations(); + + return ( + } + description={t("proxyResourcesBannerDescription")} + /> + ); +}; + +export default ProxyResourcesBanner; + diff --git a/src/components/SitesBanner.tsx b/src/components/SitesBanner.tsx new file mode 100644 index 00000000..8ba7a232 --- /dev/null +++ b/src/components/SitesBanner.tsx @@ -0,0 +1,40 @@ +"use client"; + +import React from "react"; +import { Button } from "@app/components/ui/button"; +import { Plug, ArrowRight } from "lucide-react"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import DismissableBanner from "./DismissableBanner"; + +export const SitesBanner = () => { + const t = useTranslations(); + + return ( + } + description={t("sitesBannerDescription")} + > + + + + + ); +}; + +export default SitesBanner; + diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index 71321bf8..e413207a 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -25,6 +25,7 @@ import { useRouter } from "next/navigation"; import { useMemo, useState, useTransition } from "react"; import { Badge } from "./ui/badge"; import { InfoPopup } from "./ui/info-popup"; +import ClientDownloadBanner from "./ClientDownloadBanner"; export type ClientRow = { id: number; @@ -413,6 +414,8 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { /> )} + +