Compare commits

...

14 Commits

Author SHA1 Message Date
dependabot[bot] 9b7f59b90c Bump next and @react-email/ui
Bumps [next](https://github.com/vercel/next.js) to 16.3.3 and updates ancestor dependency [@react-email/ui](https://github.com/resend/react-email/tree/HEAD/packages/ui). These dependencies need to be updated together.


Updates `next` from 16.3.0 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.0...v16.3.3)

Updates `@react-email/ui` from 6.9.3 to 6.9.5
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/ui/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/ui@6.9.5/packages/ui)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.3.3
  dependency-type: indirect
- dependency-name: "@react-email/ui"
  dependency-version: 6.9.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-11 05:24:00 +00:00
Owen Schwartz 17375348b0 Merge pull request #3702 from fosrl/dev
1.22.2
2026-09-04 17:18:42 -04:00
Owen ea9017ac06 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-09-04 17:15:33 -04:00
Owen 88770ff97b Refactor form submissions to use startTransition for improved performance 2026-09-04 17:15:20 -04:00
miloschwartz 8e75425887 update screenshots 2026-09-04 15:46:56 -04:00
Owen 44b0186044 Fix yaml import 2026-09-04 15:32:59 -04:00
Owen 063f6b5ca9 Basic DNS config send empty for olm 2026-09-04 12:31:04 -04:00
Owen Schwartz e7fdbf9e85 Merge pull request #3699 from fosrl/dev
1.22.1-s.1
2026-09-04 10:38:39 -04:00
Owen 778a840ed7 Fix react bug not making it possible to complete security form 2026-09-04 10:08:57 -04:00
Owen 9d19195089 Configurable tab title and disable flag for keys 2026-09-04 09:20:20 -04:00
Owen 54bbe82504 Dont log invalid message type
Fixes #3695
2026-09-04 09:06:58 -04:00
Milo Schwartz cddb5ecc3d Merge pull request #3691 from fosrl/dev
update readme
2026-09-03 16:19:47 -04:00
miloschwartz de57df2520 update readme and screenshots 2026-09-03 16:17:07 -04:00
Owen 9e392a967d Add AI disclosure 2026-09-03 15:41:12 -04:00
36 changed files with 220 additions and 269 deletions
+8
View File
@@ -34,6 +34,14 @@ body:
validations: validations:
required: true required: true
- type: textarea
attributes:
label: AI Disclosure
description: |
If you used AI to help write this issue, please disclose it here. This is important for transparency and helps maintain the integrity of the issue tracking process.
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Expected Behavior label: Expected Behavior
+16 -5
View File
@@ -37,11 +37,22 @@
<p align="center"> <p align="center">
<strong> <strong>
Get started with Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a> Get started with Pangolin Cloud at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
</strong> </strong>
</p> </p>
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure connectivity to infrastructure anywhere. It combines reverse-proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to private resources with NAT traversal, all with granular access control. Pangolin is an open-source SASE platform, built on WireGuard®, with a simple mission: connect and protect your users, wherever they are. It brings networking and security together as one system including a zero-trust VPN, zero-trust reverse proxy, privileged access management, and an identity-aware AI gateway, all sharing one identity and policy model. It's the same idea behind platforms like Cloudflare One, Zscaler, and Prisma but open, self-hostable, and built to stay light and easy to deploy.
### Networking and security that's unified, open, and simple
Legacy SASE platforms got the idea right: connectivity and security belong together. But they delivered it as a heavyweight, closed, cloud-locked stack assembled from years of patchwork. Pangolin exists to do that unification differently, in the open, self-hostable, and simple enough that administrators actually enjoy running it.
* **Open source, not a black box**: the code is open and auditable, so you can see exactly how your traffic is handled and how access decisions get made, instead of trusting a closed cloud control plane.
* **Networking and security as one platform**: sites, reverse proxy, client access, RBAC, and the AI gateway share one identity and policy model, so protecting users and connecting them are executed together.
* **Lightweight by design**: the whole platform is built to stay small and fast: easy to self-host on a small server, with a lightweight, user-space connector that goes in your private networks.
* **Enjoyable to use**: a clean, modern interface and a setup flow that gets out of your way, so managing access feels simple instead of like fighting a legacy admin console.
* **Zero trust from day one**: access is granted per resource, not per network, with identity provider integration, role-based access control, and full audit logging.
* **Run it your way**: self-host the Community Edition for free, step up to the Enterprise Edition for advanced features, or use Pangolin Cloud if you'd rather not manage infrastructure at all.
## Installation ## Installation
@@ -53,9 +64,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
## Deployment Options ## Deployment Options
- **Pangolin Cloud** - Fully managed service - no infrastructure required. - **Pangolin Cloud** - Fully managed service with no infrastructure required.
- **Self-Host: Community Edition** - Free, open source, and licensed under AGPL-3. - **Self-Host: Community Edition** - Free, open-source, and licensed under AGPL-3.
- **Self-Host: Enterprise Edition** - Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue. - **Self-Host: Enterprise Edition** - Open-core, and licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
## Key Features ## Key Features
+1 -1
View File
@@ -5,7 +5,7 @@ import { encrypt } from "@server/lib/crypto";
import { configFilePath1, configFilePath2 } from "@server/lib/consts"; import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { generateCA } from "@server/lib/sshCA"; import { generateCA } from "@server/lib/sshCA";
import fs from "fs"; import fs from "fs";
import yaml from "js-yaml"; import * as yaml from "js-yaml";
type GenerateOrgCaKeysArgs = { type GenerateOrgCaKeysArgs = {
orgId: string; orgId: string;
+1 -1
View File
@@ -4,7 +4,7 @@ import { encrypt, decrypt } from "@server/lib/crypto";
import { configFilePath1, configFilePath2 } from "@server/lib/consts"; import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import fs from "fs"; import fs from "fs";
import yaml from "js-yaml"; import * as yaml from "js-yaml";
type RotateServerSecretArgs = { type RotateServerSecretArgs = {
"old-secret": string; "old-secret": string;
+5 -224
View File
@@ -116,7 +116,7 @@
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "2.23.0", "@dotenvx/dotenvx": "2.23.0",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/ui": "^6.9.3", "@react-email/ui": "^6.9.5",
"@tailwindcss/postcss": "4.3.3", "@tailwindcss/postcss": "4.3.3",
"@tanstack/react-query-devtools": "5.102.8", "@tanstack/react-query-devtools": "5.102.8",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
@@ -5219,233 +5219,14 @@
} }
}, },
"node_modules/@react-email/ui": { "node_modules/@react-email/ui": {
"version": "6.9.3", "version": "6.9.5",
"resolved": "https://registry.npmjs.org/@react-email/ui/-/ui-6.9.3.tgz", "resolved": "https://registry.npmjs.org/@react-email/ui/-/ui-6.9.5.tgz",
"integrity": "sha512-6lVPkSvsRngyPkzm9w7LeUu60vlTDs/NRSYNV2SrXnjCzDCoK0ZsACK6T521Mkn8Ojft1oYg7k+4BIy1r28OGg==", "integrity": "sha512-UvE0yeSQCvdBm9BoEgvZ5vBi8abk02GbRE+4RQjjtxozy66D/O1GbUgK9RdB26+7EqQdOxdwkGLq5CMCiRO96A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "0.28.1", "esbuild": "0.28.1",
"next": "16.3.0" "next": "16.3.3"
}
},
"node_modules/@react-email/ui/node_modules/@next/env": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.0.tgz",
"integrity": "sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@react-email/ui/node_modules/@next/swc-darwin-arm64": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.0.tgz",
"integrity": "sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-darwin-x64": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.0.tgz",
"integrity": "sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.0.tgz",
"integrity": "sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-linux-arm64-musl": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.0.tgz",
"integrity": "sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-linux-x64-gnu": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.0.tgz",
"integrity": "sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-linux-x64-musl": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.0.tgz",
"integrity": "sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.0.tgz",
"integrity": "sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@next/swc-win32-x64-msvc": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.0.tgz",
"integrity": "sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@react-email/ui/node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@react-email/ui/node_modules/next": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/next/-/next-16.3.0.tgz",
"integrity": "sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/env": "16.3.0",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.5.23",
"styled-jsx": "5.1.6"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.3.0",
"@next/swc-darwin-x64": "16.3.0",
"@next/swc-linux-arm64-gnu": "16.3.0",
"@next/swc-linux-arm64-musl": "16.3.0",
"@next/swc-linux-x64-gnu": "16.3.0",
"@next/swc-linux-x64-musl": "16.3.0",
"@next/swc-win32-arm64-msvc": "16.3.0",
"@next/swc-win32-x64-msvc": "16.3.0",
"sharp": "^0.35.3"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.51.1",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
},
"sass": {
"optional": true
}
} }
}, },
"node_modules/@react-types/shared": { "node_modules/@react-types/shared": {
+1 -1
View File
@@ -139,7 +139,7 @@
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "2.23.0", "@dotenvx/dotenvx": "2.23.0",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/ui": "^6.9.3", "@react-email/ui": "^6.9.5",
"@tailwindcss/postcss": "4.3.3", "@tailwindcss/postcss": "4.3.3",
"@tanstack/react-query-devtools": "5.102.8", "@tanstack/react-query-devtools": "5.102.8",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

After

Width:  |  Height:  |  Size: 800 KiB

+5
View File
@@ -112,6 +112,11 @@ export class Config {
? "true" ? "true"
: "false"; : "false";
process.env.FLAGS_DISABLE_VIRTUAL_API_KEYS_UI = parsedConfig.flags
?.disable_virtual_api_keys_ui
? "true"
: "false";
this.rawConfig = parsedConfig; this.rawConfig = parsedConfig;
} }
+1
View File
@@ -442,6 +442,7 @@ export const configSchema = z
disable_config_managed_domains: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(),
disable_product_help_banners: z.boolean().optional(), disable_product_help_banners: z.boolean().optional(),
disable_enterprise_features: z.boolean().optional(), disable_enterprise_features: z.boolean().optional(),
disable_virtual_api_keys_ui: z.boolean().optional(),
enable_acme_cert_sync: z.boolean().optional().default(true), enable_acme_cert_sync: z.boolean().optional().default(true),
disable_private_http_placeholder: z disable_private_http_placeholder: z
.boolean() .boolean()
+2 -1
View File
@@ -104,7 +104,8 @@ const processMessage = async (
const handler = messageHandlers[message.type]; const handler = messageHandlers[message.type];
if (!handler) { if (!handler) {
throw new Error(`Unsupported message type: ${message.type}`); logger.debug(`No handler found for message type: ${message.type}`);
return;
} }
const response = await handler({ const response = await handler({
+20
View File
@@ -0,0 +1,20 @@
import config from "@server/lib/config";
// Mirrors the optional fields on the olm client's TunnelConfig - any field
// present here overrides the value the olm client is otherwise locally
// configured with; an absent field leaves the client's own config alone.
export type OlmDnsConfig = {
upstreamDns?: string[];
overrideDns?: boolean;
tunnelDns?: boolean;
matchDomains?: string[];
};
export function buildOlmDnsConfig(): OlmDnsConfig | undefined {
return {
upstreamDns: undefined,
overrideDns: undefined,
tunnelDns: undefined,
matchDomains: undefined
};
}
@@ -15,6 +15,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2"; import { sha256 } from "@oslojs/crypto/sha2";
import { getUserDeviceName } from "@server/db/names"; import { getUserDeviceName } from "@server/db/names";
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration"; import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
import { buildOlmDnsConfig } from "./dnsConfig";
import { OlmErrorCodes, sendOlmError } from "./error"; import { OlmErrorCodes, sendOlmError } from "./error";
import { handleFingerprintInsertion } from "./fingerprintingUtils"; import { handleFingerprintInsertion } from "./fingerprintingUtils";
import { build } from "@server/build"; import { build } from "@server/build";
@@ -512,6 +513,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
tunnelIP: `${clientSubnet.split("/")[0]}/${exitNode.address.split("/")[1]}` // we need to use the exit node's subnet mask here because the client will be using the exit node's subnet mask for its routing table so we can address it tunnelIP: `${clientSubnet.split("/")[0]}/${exitNode.address.split("/")[1]}` // we need to use the exit node's subnet mask here because the client will be using the exit node's subnet mask for its routing table so we can address it
} }
: undefined, : undefined,
dnsConfig: buildOlmDnsConfig(),
chainId: chainId chainId: chainId
} }
}, },
+4 -1
View File
@@ -353,7 +353,10 @@ const setupConnection = async (
const handler = messageHandlers[message.type]; const handler = messageHandlers[message.type];
if (!handler) { if (!handler) {
throw new Error(`Unsupported message type: ${message.type}`); logger.debug(
`No handler found for message type: ${message.type}`
);
return;
} }
const response = await handler({ const response = await handler({
+6 -1
View File
@@ -76,6 +76,11 @@ export default async function KeysPage(props: KeysPageProps) {
redirect("/"); redirect("/");
} }
const env = pullEnv();
if (env.flags.disableVirtualApiKeysUi) {
redirect(`/${orgId}`);
}
let keysData: ListMyVirtualApiKeysResponse | null = null; let keysData: ListMyVirtualApiKeysResponse | null = null;
try { try {
const res = await internal.get< const res = await internal.get<
@@ -90,7 +95,6 @@ export default async function KeysPage(props: KeysPageProps) {
redirect(`/${orgId}`); redirect(`/${orgId}`);
} }
const env = pullEnv();
const primaryOrg = orgs.find((o) => o.orgId === orgId)?.isPrimaryOrg; const primaryOrg = orgs.find((o) => o.orgId === orgId)?.isPrimaryOrg;
const isAdminOrOwner = Boolean(overview?.isAdmin || overview?.isOwner); const isAdminOrOwner = Boolean(overview?.isAdmin || overview?.isOwner);
@@ -106,6 +110,7 @@ export default async function KeysPage(props: KeysPageProps) {
showSidebar={false} showSidebar={false}
launcherMode launcherMode
showViewAsAdmin={isAdminOrOwner} showViewAsAdmin={isAdminOrOwner}
env={env}
> >
<UserVirtualApiKeys orgId={orgId} initialData={keysData} /> <UserVirtualApiKeys orgId={orgId} initialData={keysData} />
</Layout> </Layout>
+1
View File
@@ -83,6 +83,7 @@ export default async function OrgPage(props: OrgPageProps) {
showSidebar={false} showSidebar={false}
launcherMode launcherMode
showViewAsAdmin={isAdminOrOwner} showViewAsAdmin={isAdminOrOwner}
env={env}
> >
{overview && launcherData ? ( {overview && launcherData ? (
<ResourceLauncher <ResourceLauncher
@@ -13,7 +13,12 @@ import { StrategyOption, StrategySelect } from "@app/components/StrategySelect";
import HeaderTitle from "@app/components/SettingsSectionTitle"; import HeaderTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useActionState, useRef, useState } from "react"; import {
useActionState,
useRef,
useState,
startTransition
} from "react";
import { import {
Form, Form,
FormControl, FormControl,
@@ -518,9 +523,12 @@ export default function Page() {
<SettingsSectionForm> <SettingsSectionForm>
<Form {...internalForm}> <Form {...internalForm}>
<form <form
action={ onSubmit={(e) => {
submitInternalAction e.preventDefault();
} startTransition(() => {
submitInternalAction();
});
}}
className="space-y-4" className="space-y-4"
id="create-user-form" id="create-user-form"
> >
+12 -2
View File
@@ -3,7 +3,12 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { useState, useTransition, useActionState } from "react"; import {
useState,
useTransition,
useActionState,
startTransition
} from "react";
import { import {
Form, Form,
FormControl, FormControl,
@@ -243,7 +248,12 @@ function GeneralSectionForm({ org }: SectionFormProps) {
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}> <Form {...form}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
className="grid gap-4" className="grid gap-4"
id="org-general-settings-form" id="org-general-settings-form"
> >
@@ -3,7 +3,13 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { useState, useRef, useActionState, type ComponentRef } from "react"; import {
useState,
useRef,
useActionState,
startTransition,
type ComponentRef
} from "react";
import { import {
Form, Form,
FormControl, FormControl,
@@ -117,6 +123,7 @@ export default function SecurityPage() {
} }
function LogRetentionSectionForm({ org }: SectionFormProps) { function LogRetentionSectionForm({ org }: SectionFormProps) {
const { updateOrg } = useOrgContext();
const form = useForm({ const form = useForm({
resolver: zodResolver( resolver: zodResolver(
SecurityFormSchema.pick({ SecurityFormSchema.pick({
@@ -173,6 +180,11 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
// Update organization // Update organization
await api.post(`/org/${org.orgId}`, reqData); await api.post(`/org/${org.orgId}`, reqData);
// Update the org context immediately so the dropdowns reflect
// the saved values without waiting on a re-fetch that could
// race a lagging read replica
updateOrg(reqData);
toast({ toast({
title: t("orgUpdated"), title: t("orgUpdated"),
description: t("orgUpdatedDescription") description: t("orgUpdatedDescription")
@@ -199,7 +211,12 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}> <Form {...form}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
className="grid gap-4" className="grid gap-4"
id="org-log-retention-settings-form" id="org-log-retention-settings-form"
> >
@@ -827,6 +844,7 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
function SecuritySettingsSectionForm({ org }: SectionFormProps) { function SecuritySettingsSectionForm({ org }: SectionFormProps) {
const router = useRouter(); const router = useRouter();
const { updateOrg } = useOrgContext();
const form = useForm({ const form = useForm({
resolver: zodResolver( resolver: zodResolver(
SecurityFormSchema.pick({ SecurityFormSchema.pick({
@@ -899,6 +917,11 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
// Update organization // Update organization
await api.post(`/org/${org.orgId}`, reqData); await api.post(`/org/${org.orgId}`, reqData);
// Update the org context immediately so the dropdowns reflect
// the saved values without waiting on a re-fetch that could
// race a lagging read replica
updateOrg(reqData);
toast({ toast({
title: t("orgUpdated"), title: t("orgUpdated"),
description: t("orgUpdatedDescription") description: t("orgUpdatedDescription")
@@ -942,7 +965,12 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
<SettingsSectionForm> <SettingsSectionForm>
<Form {...form}> <Form {...form}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
ref={formRef} ref={formRef}
id="security-settings-section-form" id="security-settings-section-form"
className="space-y-4" className="space-y-4"
@@ -41,7 +41,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useActionState, useEffect, useMemo } from "react"; import { useActionState, useEffect, useMemo, startTransition } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -232,7 +232,12 @@ export default function PrivateResourceInferencePage() {
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<Form {...form}> <Form {...form}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
id="private-resource-providers-form" id="private-resource-providers-form"
> >
<SettingsFormGrid> <SettingsFormGrid>
@@ -29,7 +29,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { useActionState, useMemo } from "react"; import { useActionState, useMemo, startTransition } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource"; import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource";
@@ -97,7 +97,12 @@ export default function PrivateResourceGeneralPage() {
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<Form {...form}> <Form {...form}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
id="private-resource-general-form" id="private-resource-general-form"
> >
<SettingsFormGrid> <SettingsFormGrid>
@@ -41,7 +41,13 @@ import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { toASCII, toUnicode } from "punycode"; import { toASCII, toUnicode } from "punycode";
import { useActionState, useEffect, useMemo, useState } from "react"; import {
useActionState,
useEffect,
useMemo,
useState,
startTransition
} from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod"; import z from "zod";
@@ -282,7 +288,12 @@ export default function GeneralForm() {
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<Form {...form}> <Form {...form}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
id="general-settings-form" id="general-settings-form"
> >
<SettingsFormGrid> <SettingsFormGrid>
@@ -41,7 +41,7 @@ import {
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useActionState, useState } from "react"; import { useActionState, useState, startTransition } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -209,7 +209,15 @@ function ProxyResourceHttpForm({
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<Form {...form}> <Form {...form}>
<form action={formAction} id="http-settings-form"> <form
onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
id="http-settings-form"
>
<SettingsFormGrid> <SettingsFormGrid>
{!env.flags.usePangolinDns && ( {!env.flags.usePangolinDns && (
<SettingsFormCell span="full"> <SettingsFormCell span="full">
@@ -37,7 +37,7 @@ import { AxiosResponse } from "axios";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useActionState, useEffect } from "react"; import { useActionState, useEffect, startTransition } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod"; import z from "zod";
@@ -203,7 +203,12 @@ export default function ResourceMaintenancePage() {
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<Form {...maintenanceForm}> <Form {...maintenanceForm}>
<form <form
action={maintenanceFormAction} onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
maintenanceFormAction();
});
}}
id="maintenance-settings-form" id="maintenance-settings-form"
> >
<SettingsFormGrid> <SettingsFormGrid>
@@ -48,7 +48,8 @@ import { useRouter } from "next/navigation";
import { import {
use, use,
useActionState, useActionState,
useMemo useMemo,
startTransition
} from "react"; } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -206,7 +207,12 @@ function ProxyResourceProtocolForm({
<SettingsSectionForm variant="half"> <SettingsSectionForm variant="half">
<Form {...proxySettingsForm}> <Form {...proxySettingsForm}>
<form <form
action={formAction} onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
id="proxy-protocol-settings-form" id="proxy-protocol-settings-form"
> >
<SettingsFormGrid> <SettingsFormGrid>
@@ -30,9 +30,15 @@ import { normalizePostAuthPath } from "@server/lib/normalizePostAuthPath";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export async function generateMetadata(): Promise<Metadata> {
title: "Resource Access" const env = pullEnv();
}; const title =
env.branding.resourceAuthPage?.titleText ||
env.branding.appName ||
"Resource Access";
return { title };
}
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
+10 -6
View File
@@ -53,17 +53,21 @@ export type OrgNavSectionsOptions = {
}; };
// Merged from 'user-management-and-resources' branch // Merged from 'user-management-and-resources' branch
export const orgLangingNavItems: SidebarNavItem[] = [ export const orgLangingNavItems = (env?: Env): SidebarNavItem[] => [
{ {
title: "sidebarAccount", title: "sidebarAccount",
href: "/{orgId}", href: "/{orgId}",
icon: <LayoutGrid className="size-4 flex-none" /> icon: <LayoutGrid className="size-4 flex-none" />
}, },
{ ...(!env?.flags.disableVirtualApiKeysUi
title: "sidebarMyApiKeys", ? [
href: "/{orgId}/keys", {
icon: <KeyRound className="size-4 flex-none" /> title: "sidebarMyApiKeys",
} href: "/{orgId}/keys",
icon: <KeyRound className="size-4 flex-none" />
}
]
: [])
]; ];
export const orgNavSections = ( export const orgNavSections = (
+5 -2
View File
@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org"; import { ListUserOrgsResponse } from "@server/routers/org";
import { Env } from "@app/lib/types/env";
import { import {
orgLangingNavItems, orgLangingNavItems,
type CommandBarNavSection, type CommandBarNavSection,
@@ -25,6 +26,7 @@ interface LayoutProps {
defaultSidebarCollapsed?: boolean; defaultSidebarCollapsed?: boolean;
launcherMode?: boolean; launcherMode?: boolean;
showViewAsAdmin?: boolean; showViewAsAdmin?: boolean;
env?: Env;
} }
export async function Layout({ export async function Layout({
@@ -38,7 +40,8 @@ export async function Layout({
showTopBar = true, showTopBar = true,
defaultSidebarCollapsed = false, defaultSidebarCollapsed = false,
launcherMode = false, launcherMode = false,
showViewAsAdmin = false showViewAsAdmin = false,
env
}: LayoutProps) { }: LayoutProps) {
const allCookies = await cookies(); const allCookies = await cookies();
const sidebarStateCookie = allCookies.get("pangolin-sidebar-state")?.value; const sidebarStateCookie = allCookies.get("pangolin-sidebar-state")?.value;
@@ -49,7 +52,7 @@ export async function Layout({
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed); (sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
const launcherNavItems: SidebarNavItem[] = launcherMode const launcherNavItems: SidebarNavItem[] = launcherMode
? orgLangingNavItems ? orgLangingNavItems(env)
: []; : [];
return ( return (
+2 -1
View File
@@ -62,7 +62,8 @@ export function OrgLabelForm({
<form <form
id="org-label-form" id="org-label-form"
className="flex flex-col gap-4 px-0.5" className="flex flex-col gap-4 px-0.5"
action={async () => { onSubmit={async (e) => {
e.preventDefault();
if (await form.trigger()) { if (await form.trigger()) {
onSubmit(form.getValues()); onSubmit(form.getValues());
} }
@@ -211,7 +211,15 @@ export default function AlertRuleGraphEditor({
return ( return (
<Form {...form}> <Form {...form}>
<form id={FORM_ID} action={formAction}> <form
id={FORM_ID}
onSubmit={(e) => {
e.preventDefault();
startTransition(() => {
formAction();
});
}}
>
<SettingsContainer> <SettingsContainer>
<PaidFeaturesAlert tiers={tierMatrix.alertingRules} /> <PaidFeaturesAlert tiers={tierMatrix.alertingRules} />
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8 items-start"> <div className="flex flex-col lg:flex-row gap-6 lg:gap-8 items-start">
+4
View File
@@ -70,6 +70,10 @@ export function pullEnv(): Env {
: false, : false,
disableEnterpriseFeatures: disableEnterpriseFeatures:
process.env.DISABLE_ENTERPRISE_FEATURES === "true" process.env.DISABLE_ENTERPRISE_FEATURES === "true"
? true
: false,
disableVirtualApiKeysUi:
process.env.FLAGS_DISABLE_VIRTUAL_API_KEYS_UI === "true"
? true ? true
: false : false
}, },
+1
View File
@@ -36,6 +36,7 @@ export type Env = {
usePangolinDns: boolean; usePangolinDns: boolean;
disableProductHelpBanners: boolean; disableProductHelpBanners: boolean;
disableEnterpriseFeatures: boolean; disableEnterpriseFeatures: boolean;
disableVirtualApiKeysUi: boolean;
}; };
branding: { branding: {
appName?: string; appName?: string;