mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-22 21:06:37 +00:00
Compare commits
16 Commits
1.13.0.s.0
...
1.13.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2d4c2f83c | ||
|
|
25fed23758 | ||
|
|
5cb3fa1127 | ||
|
|
deac26bad2 | ||
|
|
c7747fd4b4 | ||
|
|
1aaad43871 | ||
|
|
143175bde7 | ||
|
|
9f55d6b20a | ||
|
|
4366ca5836 | ||
|
|
d5307adef0 | ||
|
|
3d857c3b52 | ||
|
|
a012369f83 | ||
|
|
ba99614d58 | ||
|
|
27db77bca4 | ||
|
|
29b924230f | ||
|
|
8eb3f6aacc |
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
@@ -107,7 +107,7 @@ jobs:
|
|||||||
- name: Build and push Docker images (Docker Hub)
|
- name: Build and push Docker images (Docker Hub)
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
make build-release tag=$TAG
|
make -j4 build-release tag=$TAG
|
||||||
echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
|||||||
39
.github/workflows/restart-runners.yml
vendored
Normal file
39
.github/workflows/restart-runners.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Restart Runners
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 */7 * *'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ec2-maintenance-prod:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v2
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Start EC2 instance
|
||||||
|
run: |
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||||
|
echo "EC2 instances started"
|
||||||
|
|
||||||
|
- name: Wait
|
||||||
|
run: sleep 600
|
||||||
|
|
||||||
|
- name: Stop EC2 instance
|
||||||
|
run: |
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||||
|
echo "EC2 instances stopped"
|
||||||
31
Makefile
31
Makefile
@@ -1,8 +1,13 @@
|
|||||||
.PHONY: build build-pg build-release build-arm build-x86 test clean
|
.PHONY: build dev-build-sqlite dev-build-pg build-release build-arm build-x86 test clean
|
||||||
|
|
||||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
||||||
build-release:
|
|
||||||
|
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||||
|
|
||||||
|
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||||
|
|
||||||
|
build-sqlite:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
@@ -16,6 +21,12 @@ build-release:
|
|||||||
--tag fosrl/pangolin:$(minor_tag) \
|
--tag fosrl/pangolin:$(minor_tag) \
|
||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-postgresql:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
@@ -25,6 +36,12 @@ build-release:
|
|||||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-ee-sqlite:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
@@ -34,6 +51,12 @@ build-release:
|
|||||||
--tag fosrl/pangolin:ee-$(minor_tag) \
|
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-ee-postgresql:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
@@ -80,10 +103,10 @@ build-arm:
|
|||||||
build-x86:
|
build-x86:
|
||||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-sqlite:
|
dev-build-sqlite:
|
||||||
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-pg:
|
dev-build-pg:
|
||||||
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -2272,5 +2272,8 @@
|
|||||||
"remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.",
|
"remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.",
|
||||||
"remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?",
|
"remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?",
|
||||||
"remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.",
|
"remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.",
|
||||||
"agent": "Agent"
|
"agent": "Agent",
|
||||||
|
"personalUseOnly": "Personal Use Only",
|
||||||
|
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
|
||||||
|
"instanceIsUnlicensed": "This instance is unlicensed."
|
||||||
}
|
}
|
||||||
|
|||||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -75,7 +75,7 @@
|
|||||||
"lucide-react": "0.559.0",
|
"lucide-react": "0.559.0",
|
||||||
"maxmind": "5.0.1",
|
"maxmind": "5.0.1",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.7",
|
"next": "15.5.9",
|
||||||
"next-intl": "4.5.8",
|
"next-intl": "4.5.8",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
@@ -88,9 +88,9 @@
|
|||||||
"pg": "8.16.3",
|
"pg": "8.16.3",
|
||||||
"posthog-node": "5.17.2",
|
"posthog-node": "5.17.2",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.1",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "9.12.0",
|
"react-day-picker": "9.12.0",
|
||||||
"react-dom": "19.2.1",
|
"react-dom": "19.2.3",
|
||||||
"react-easy-sort": "1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.68.0",
|
"react-hook-form": "7.68.0",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
@@ -3835,9 +3835,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "15.5.7",
|
"version": "15.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
|
||||||
"integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
|
"integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
@@ -16291,13 +16291,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "15.5.7",
|
"version": "15.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
|
||||||
"integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
|
"integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "15.5.7",
|
"@next/env": "15.5.9",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
@@ -19737,9 +19737,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.1",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -19768,16 +19768,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.1",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.2.1"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-easy-sort": {
|
"node_modules/react-easy-sort": {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
"lucide-react": "0.559.0",
|
"lucide-react": "0.559.0",
|
||||||
"maxmind": "5.0.1",
|
"maxmind": "5.0.1",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.7",
|
"next": "15.5.9",
|
||||||
"next-intl": "4.5.8",
|
"next-intl": "4.5.8",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
@@ -112,9 +112,9 @@
|
|||||||
"pg": "8.16.3",
|
"pg": "8.16.3",
|
||||||
"posthog-node": "5.17.2",
|
"posthog-node": "5.17.2",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.1",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "9.12.0",
|
"react-day-picker": "9.12.0",
|
||||||
"react-dom": "19.2.1",
|
"react-dom": "19.2.3",
|
||||||
"react-easy-sort": "1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.68.0",
|
"react-hook-form": "7.68.0",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
@@ -178,4 +178,4 @@
|
|||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.49.0"
|
"typescript-eslint": "8.49.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.13.0";
|
export const APP_VERSION = "1.13.1";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
@@ -120,11 +120,13 @@ function bigIntToIp(num: bigint, version: IPVersion): string {
|
|||||||
* Parses an endpoint string (ip:port) handling both IPv4 and IPv6 addresses.
|
* Parses an endpoint string (ip:port) handling both IPv4 and IPv6 addresses.
|
||||||
* IPv6 addresses may be bracketed like [::1]:8080 or unbracketed like ::1:8080.
|
* IPv6 addresses may be bracketed like [::1]:8080 or unbracketed like ::1:8080.
|
||||||
* For unbracketed IPv6, the last colon-separated segment is treated as the port.
|
* For unbracketed IPv6, the last colon-separated segment is treated as the port.
|
||||||
*
|
*
|
||||||
* @param endpoint The endpoint string to parse (e.g., "192.168.1.1:8080" or "[::1]:8080" or "2607:fea8::1:8080")
|
* @param endpoint The endpoint string to parse (e.g., "192.168.1.1:8080" or "[::1]:8080" or "2607:fea8::1:8080")
|
||||||
* @returns An object with ip and port, or null if parsing fails
|
* @returns An object with ip and port, or null if parsing fails
|
||||||
*/
|
*/
|
||||||
export function parseEndpoint(endpoint: string): { ip: string; port: number } | null {
|
export function parseEndpoint(
|
||||||
|
endpoint: string
|
||||||
|
): { ip: string; port: number } | null {
|
||||||
if (!endpoint) return null;
|
if (!endpoint) return null;
|
||||||
|
|
||||||
// Check for bracketed IPv6 format: [ip]:port
|
// Check for bracketed IPv6 format: [ip]:port
|
||||||
@@ -138,7 +140,7 @@ export function parseEndpoint(endpoint: string): { ip: string; port: number } |
|
|||||||
|
|
||||||
// Check if this looks like IPv6 (contains multiple colons)
|
// Check if this looks like IPv6 (contains multiple colons)
|
||||||
const colonCount = (endpoint.match(/:/g) || []).length;
|
const colonCount = (endpoint.match(/:/g) || []).length;
|
||||||
|
|
||||||
if (colonCount > 1) {
|
if (colonCount > 1) {
|
||||||
// This is IPv6 - the port is after the last colon
|
// This is IPv6 - the port is after the last colon
|
||||||
const lastColonIndex = endpoint.lastIndexOf(":");
|
const lastColonIndex = endpoint.lastIndexOf(":");
|
||||||
@@ -163,7 +165,7 @@ export function parseEndpoint(endpoint: string): { ip: string; port: number } |
|
|||||||
/**
|
/**
|
||||||
* Formats an IP and port into a consistent endpoint string.
|
* Formats an IP and port into a consistent endpoint string.
|
||||||
* IPv6 addresses are wrapped in brackets for proper parsing.
|
* IPv6 addresses are wrapped in brackets for proper parsing.
|
||||||
*
|
*
|
||||||
* @param ip The IP address (IPv4 or IPv6)
|
* @param ip The IP address (IPv4 or IPv6)
|
||||||
* @param port The port number
|
* @param port The port number
|
||||||
* @returns Formatted endpoint string
|
* @returns Formatted endpoint string
|
||||||
@@ -430,7 +432,12 @@ export function generateRemoteSubnets(
|
|||||||
): string[] {
|
): string[] {
|
||||||
const remoteSubnets = allSiteResources
|
const remoteSubnets = allSiteResources
|
||||||
.filter((sr) => {
|
.filter((sr) => {
|
||||||
if (sr.mode === "cidr") return true;
|
if (sr.mode === "cidr") {
|
||||||
|
// check if its a valid CIDR using zod
|
||||||
|
const cidrSchema = z.union([z.cidrv4(), z.cidrv6()]);
|
||||||
|
const parseResult = cidrSchema.safeParse(sr.destination);
|
||||||
|
return parseResult.success;
|
||||||
|
}
|
||||||
if (sr.mode === "host") {
|
if (sr.mode === "host") {
|
||||||
// check if its a valid IP using zod
|
// check if its a valid IP using zod
|
||||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||||
@@ -454,13 +461,12 @@ export function generateRemoteSubnets(
|
|||||||
export type Alias = { alias: string | null; aliasAddress: string | null };
|
export type Alias = { alias: string | null; aliasAddress: string | null };
|
||||||
|
|
||||||
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||||
let aliasConfigs = allSiteResources
|
return allSiteResources
|
||||||
.filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host")
|
.filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host")
|
||||||
.map((sr) => ({
|
.map((sr) => ({
|
||||||
alias: sr.alias,
|
alias: sr.alias,
|
||||||
aliasAddress: sr.aliasAddress
|
aliasAddress: sr.aliasAddress
|
||||||
}));
|
}));
|
||||||
return aliasConfigs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubnetProxyTarget = {
|
export type SubnetProxyTarget = {
|
||||||
|
|||||||
@@ -955,28 +955,8 @@ export async function rebuildClientAssociationsFromClient(
|
|||||||
|
|
||||||
/////////// Send messages ///////////
|
/////////// Send messages ///////////
|
||||||
|
|
||||||
// Get the olm for this client
|
|
||||||
const [olm] = await trx
|
|
||||||
.select({ olmId: olms.olmId })
|
|
||||||
.from(olms)
|
|
||||||
.where(eq(olms.clientId, client.clientId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!olm) {
|
|
||||||
logger.warn(
|
|
||||||
`Olm not found for client ${client.clientId}, skipping peer updates`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle messages for sites being added
|
// Handle messages for sites being added
|
||||||
await handleMessagesForClientSites(
|
await handleMessagesForClientSites(client, sitesToAdd, sitesToRemove, trx);
|
||||||
client,
|
|
||||||
olm.olmId,
|
|
||||||
sitesToAdd,
|
|
||||||
sitesToRemove,
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle subnet proxy target updates for resources
|
// Handle subnet proxy target updates for resources
|
||||||
await handleMessagesForClientResources(
|
await handleMessagesForClientResources(
|
||||||
@@ -996,11 +976,26 @@ async function handleMessagesForClientSites(
|
|||||||
userId: string | null;
|
userId: string | null;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
},
|
},
|
||||||
olmId: string,
|
|
||||||
sitesToAdd: number[],
|
sitesToAdd: number[],
|
||||||
sitesToRemove: number[],
|
sitesToRemove: number[],
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// Get the olm for this client
|
||||||
|
const [olm] = await trx
|
||||||
|
.select({ olmId: olms.olmId })
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, client.clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!olm) {
|
||||||
|
logger.warn(
|
||||||
|
`Olm not found for client ${client.clientId}, skipping peer updates`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const olmId = olm.olmId;
|
||||||
|
|
||||||
if (!client.subnet || !client.pubKey) {
|
if (!client.subnet || !client.pubKey) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Client ${client.clientId} missing subnet or pubKey, skipping peer updates`
|
`Client ${client.clientId} missing subnet or pubKey, skipping peer updates`
|
||||||
@@ -1021,9 +1016,9 @@ async function handleMessagesForClientSites(
|
|||||||
.leftJoin(newts, eq(sites.siteId, newts.siteId))
|
.leftJoin(newts, eq(sites.siteId, newts.siteId))
|
||||||
.where(inArray(sites.siteId, allSiteIds));
|
.where(inArray(sites.siteId, allSiteIds));
|
||||||
|
|
||||||
let newtJobs: Promise<any>[] = [];
|
const newtJobs: Promise<any>[] = [];
|
||||||
let olmJobs: Promise<any>[] = [];
|
const olmJobs: Promise<any>[] = [];
|
||||||
let exitNodeJobs: Promise<any>[] = [];
|
const exitNodeJobs: Promise<any>[] = [];
|
||||||
|
|
||||||
for (const siteData of sitesData) {
|
for (const siteData of sitesData) {
|
||||||
const site = siteData.sites;
|
const site = siteData.sites;
|
||||||
@@ -1130,18 +1125,8 @@ async function handleMessagesForClientResources(
|
|||||||
resourcesToRemove: number[],
|
resourcesToRemove: number[],
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Group resources by site
|
const proxyJobs: Promise<any>[] = [];
|
||||||
const resourcesBySite = new Map<number, SiteResource[]>();
|
const olmJobs: Promise<any>[] = [];
|
||||||
|
|
||||||
for (const resource of allNewResources) {
|
|
||||||
if (!resourcesBySite.has(resource.siteId)) {
|
|
||||||
resourcesBySite.set(resource.siteId, []);
|
|
||||||
}
|
|
||||||
resourcesBySite.get(resource.siteId)!.push(resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
let proxyJobs: Promise<any>[] = [];
|
|
||||||
let olmJobs: Promise<any>[] = [];
|
|
||||||
|
|
||||||
// Handle additions
|
// Handle additions
|
||||||
if (resourcesToAdd.length > 0) {
|
if (resourcesToAdd.length > 0) {
|
||||||
|
|||||||
@@ -84,14 +84,11 @@ LQIDAQAB
|
|||||||
-----END PUBLIC KEY-----`;
|
-----END PUBLIC KEY-----`;
|
||||||
|
|
||||||
constructor(private hostMeta: HostMeta) {
|
constructor(private hostMeta: HostMeta) {
|
||||||
setInterval(
|
setInterval(async () => {
|
||||||
async () => {
|
this.doRecheck = true;
|
||||||
this.doRecheck = true;
|
await this.check();
|
||||||
await this.check();
|
this.doRecheck = false;
|
||||||
this.doRecheck = false;
|
}, 1000 * this.phoneHomeInterval);
|
||||||
},
|
|
||||||
1000 * this.phoneHomeInterval
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public listKeys(): LicenseKeyCache[] {
|
public listKeys(): LicenseKeyCache[] {
|
||||||
@@ -242,7 +239,9 @@ LQIDAQAB
|
|||||||
// First failure: fail silently
|
// First failure: fail silently
|
||||||
logger.error("Error communicating with license server:");
|
logger.error("Error communicating with license server:");
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
logger.error(`Allowing failure. Will retry one more time at next run interval.`);
|
logger.error(
|
||||||
|
`Allowing failure. Will retry one more time at next run interval.`
|
||||||
|
);
|
||||||
// return last known good status
|
// return last known good status
|
||||||
return this.statusCache.get(
|
return this.statusCache.get(
|
||||||
this.statusKey
|
this.statusKey
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logRequestAudit(
|
export async function logRequestAudit(
|
||||||
data: {
|
data: {
|
||||||
action: boolean;
|
action: boolean;
|
||||||
reason: number;
|
reason: number;
|
||||||
@@ -174,14 +174,13 @@ export function logRequestAudit(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Quick synchronous check - if org has 0 retention, skip immediately
|
// Check retention before buffering any logs
|
||||||
if (data.orgId) {
|
if (data.orgId) {
|
||||||
const cached = cache.get<number>(`org_${data.orgId}_retentionDays`);
|
const retentionDays = await getRetentionDays(data.orgId);
|
||||||
if (cached === 0) {
|
if (retentionDays === 0) {
|
||||||
// do not log
|
// do not log
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If not cached or > 0, we'll log it (async retention check happens in background)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let actorType: string | undefined;
|
let actorType: string | undefined;
|
||||||
@@ -261,16 +260,6 @@ export function logRequestAudit(
|
|||||||
} else {
|
} else {
|
||||||
scheduleFlush();
|
scheduleFlush();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Async retention check in background (don't await)
|
|
||||||
if (
|
|
||||||
data.orgId &&
|
|
||||||
cache.get<number>(`org_${data.orgId}_retentionDays`) === undefined
|
|
||||||
) {
|
|
||||||
getRetentionDays(data.orgId).catch((err) =>
|
|
||||||
logger.error("Error checking retention days:", err)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export default function ExitNodesTable({
|
|||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("remoteExitNodeQuestionRemove")}</p>
|
<p>{t("remoteExitNodeQuestionRemove")}</p>
|
||||||
|
|
||||||
<p>{t("remoteExitNodeMessageRemove")}</p>
|
<p>{t("remoteExitNodeMessageRemove")}</p>
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export default function GeneralPage() {
|
|||||||
setIsDeleteModalOpen(val);
|
setIsDeleteModalOpen(val);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("orgQuestionRemove")}</p>
|
<p>{t("orgQuestionRemove")}</p>
|
||||||
<p>{t("orgMessageRemove")}</p>
|
<p>{t("orgMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,7 +303,7 @@ export default function GeneralPage() {
|
|||||||
open={isSecurityPolicyConfirmOpen}
|
open={isSecurityPolicyConfirmOpen}
|
||||||
setOpen={setIsSecurityPolicyConfirmOpen}
|
setOpen={setIsSecurityPolicyConfirmOpen}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("securityPolicyChangeDescription")}</p>
|
<p>{t("securityPolicyChangeDescription")}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -449,15 +449,16 @@ export default function ResourceRules(props: {
|
|||||||
type="number"
|
type="number"
|
||||||
onClick={(e) => e.currentTarget.focus()}
|
onClick={(e) => e.currentTarget.focus()}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
const parsed = z
|
const parsed = z.coerce
|
||||||
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.optional()
|
.optional()
|
||||||
.safeParse(e.target.value);
|
.safeParse(e.target.value);
|
||||||
|
|
||||||
if (!parsed.data) {
|
if (!parsed.success) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("rulesErrorInvalidIpAddress"), // correct priority or IP?
|
title: t("rulesErrorInvalidPriority"), // correct priority or IP?
|
||||||
description: t(
|
description: t(
|
||||||
"rulesErrorInvalidPriorityDescription"
|
"rulesErrorInvalidPriorityDescription"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ export default function LicensePage() {
|
|||||||
setSelectedLicenseKey(null);
|
setSelectedLicenseKey(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("licenseQuestionRemove")}</p>
|
<p>{t("licenseQuestionRemove")}</p>
|
||||||
<p>
|
<p>
|
||||||
<b>{t("licenseMessageRemove")}</b>
|
<b>{t("licenseMessageRemove")}</b>
|
||||||
@@ -360,7 +360,8 @@ export default function LicensePage() {
|
|||||||
<div className="space-y-2 text-green-500">
|
<div className="space-y-2 text-green-500">
|
||||||
<div className="text-2xl flex items-center gap-2">
|
<div className="text-2xl flex items-center gap-2">
|
||||||
<Check />
|
<Check />
|
||||||
{t("licensed")}
|
{t("licensed") +
|
||||||
|
`${licenseStatus?.tier === "personal" ? ` (${t("personalUseOnly")})` : ""}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ export default function UsersTable({ users }: Props) {
|
|||||||
setSelected(null);
|
setSelected(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("userQuestionRemove")}</p>
|
<p>{t("userQuestionRemove")}</p>
|
||||||
|
|
||||||
<p>{t("userMessageRemove")}</p>
|
<p>{t("userMessageRemove")}</p>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
|
|||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
let hideFooter = false;
|
let hideFooter = false;
|
||||||
|
|
||||||
|
let licenseStatus: GetLicenseStatusResponse | null = null;
|
||||||
if (build == "enterprise") {
|
if (build == "enterprise") {
|
||||||
const licenseStatusRes = await cache(
|
const licenseStatusRes = await cache(
|
||||||
async () =>
|
async () =>
|
||||||
@@ -30,10 +31,12 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
|
|||||||
"/license/status"
|
"/license/status"
|
||||||
)
|
)
|
||||||
)();
|
)();
|
||||||
|
licenseStatus = licenseStatusRes.data.data;
|
||||||
if (
|
if (
|
||||||
env.branding.hideAuthLayoutFooter &&
|
env.branding.hideAuthLayoutFooter &&
|
||||||
licenseStatusRes.data.data.isHostLicensed &&
|
licenseStatusRes.data.data.isHostLicensed &&
|
||||||
licenseStatusRes.data.data.isLicenseValid
|
licenseStatusRes.data.data.isLicenseValid &&
|
||||||
|
licenseStatusRes.data.data.tier !== "personal"
|
||||||
) {
|
) {
|
||||||
hideFooter = true;
|
hideFooter = true;
|
||||||
}
|
}
|
||||||
@@ -83,6 +86,23 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
|
|||||||
? t("enterpriseEdition")
|
? t("enterpriseEdition")
|
||||||
: t("pangolinCloud")}
|
: t("pangolinCloud")}
|
||||||
</span>
|
</span>
|
||||||
|
{build === "enterprise" &&
|
||||||
|
licenseStatus?.isHostLicensed &&
|
||||||
|
licenseStatus?.isLicenseValid &&
|
||||||
|
licenseStatus?.tier === "personal" ? (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span>{t("personalUseOnly")}</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{build === "enterprise" &&
|
||||||
|
(!licenseStatus?.isHostLicensed ||
|
||||||
|
!licenseStatus?.isLicenseValid) ? (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span>{t("unlicensed")}</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{build === "saas" && (
|
{build === "saas" && (
|
||||||
<>
|
<>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export default function IdpTable({ idps }: Props) {
|
|||||||
setSelectedIdp(null);
|
setSelectedIdp(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>
|
<p>
|
||||||
{t("idpQuestionRemove", {
|
{t("idpQuestionRemove", {
|
||||||
name: selectedIdp.name
|
name: selectedIdp.name
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ export default function UsersTable({ users }: Props) {
|
|||||||
setSelected(null);
|
setSelected(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>
|
<p>
|
||||||
{t("userQuestionRemove", {
|
{t("userQuestionRemove", {
|
||||||
selectedUser:
|
selectedUser:
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
|||||||
setSelected(null);
|
setSelected(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("apiKeysQuestionRemove")}</p>
|
<p>{t("apiKeysQuestionRemove")}</p>
|
||||||
|
|
||||||
<p>{t("apiKeysMessageRemove")}</p>
|
<p>{t("apiKeysMessageRemove")}</p>
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ export default function ClientResourcesTable({
|
|||||||
setSelectedInternalResource(null);
|
setSelectedInternalResource(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("resourceQuestionRemove")}</p>
|
<p>{t("resourceQuestionRemove")}</p>
|
||||||
<p>{t("resourceMessageRemove")}</p>
|
<p>{t("resourceMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -177,7 +177,13 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
|||||||
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaFooter className={cn("mt-8 md:mt-0 -mx-6 px-6 pt-6 border-t border-border", className)} {...props}>
|
<CredenzaFooter
|
||||||
|
className={cn(
|
||||||
|
"mt-8 md:mt-0 -mx-6 px-6 pt-6 border-t border-border",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ interface DataTablePaginationProps<TData> {
|
|||||||
isServerPagination?: boolean;
|
isServerPagination?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
pageSize?: number;
|
||||||
|
pageIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTablePagination<TData>({
|
export function DataTablePagination<TData>({
|
||||||
@@ -33,10 +35,26 @@ export function DataTablePagination<TData>({
|
|||||||
totalCount,
|
totalCount,
|
||||||
isServerPagination = false,
|
isServerPagination = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
disabled = false
|
disabled = false,
|
||||||
|
pageSize: controlledPageSize,
|
||||||
|
pageIndex: controlledPageIndex
|
||||||
}: DataTablePaginationProps<TData>) {
|
}: DataTablePaginationProps<TData>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
|
// Use controlled values if provided, otherwise fall back to table state
|
||||||
|
const pageSize = controlledPageSize ?? table.getState().pagination.pageSize;
|
||||||
|
const pageIndex =
|
||||||
|
controlledPageIndex ?? table.getState().pagination.pageIndex;
|
||||||
|
|
||||||
|
// Calculate page boundaries based on controlled state
|
||||||
|
// For server-side pagination, use totalCount if available for accurate page count
|
||||||
|
const pageCount =
|
||||||
|
isServerPagination && totalCount !== undefined
|
||||||
|
? Math.ceil(totalCount / pageSize)
|
||||||
|
: table.getPageCount();
|
||||||
|
const canNextPage = pageIndex < pageCount - 1;
|
||||||
|
const canPreviousPage = pageIndex > 0;
|
||||||
|
|
||||||
const handlePageSizeChange = (value: string) => {
|
const handlePageSizeChange = (value: string) => {
|
||||||
const newPageSize = Number(value);
|
const newPageSize = Number(value);
|
||||||
table.setPageSize(newPageSize);
|
table.setPageSize(newPageSize);
|
||||||
@@ -51,7 +69,7 @@ export function DataTablePagination<TData>({
|
|||||||
action: "first" | "previous" | "next" | "last"
|
action: "first" | "previous" | "next" | "last"
|
||||||
) => {
|
) => {
|
||||||
if (isServerPagination && onPageChange) {
|
if (isServerPagination && onPageChange) {
|
||||||
const currentPage = table.getState().pagination.pageIndex;
|
const currentPage = pageIndex;
|
||||||
const pageCount = table.getPageCount();
|
const pageCount = table.getPageCount();
|
||||||
|
|
||||||
let newPage: number;
|
let newPage: number;
|
||||||
@@ -77,18 +95,24 @@ export function DataTablePagination<TData>({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use table's built-in navigation for client-side pagination
|
// Use table's built-in navigation for client-side pagination
|
||||||
|
// But add bounds checking to prevent going beyond page boundaries
|
||||||
|
const pageCount = table.getPageCount();
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "first":
|
case "first":
|
||||||
table.setPageIndex(0);
|
table.setPageIndex(0);
|
||||||
break;
|
break;
|
||||||
case "previous":
|
case "previous":
|
||||||
table.previousPage();
|
if (pageIndex > 0) {
|
||||||
|
table.previousPage();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "next":
|
case "next":
|
||||||
table.nextPage();
|
if (pageIndex < pageCount - 1) {
|
||||||
|
table.nextPage();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "last":
|
case "last":
|
||||||
table.setPageIndex(table.getPageCount() - 1);
|
table.setPageIndex(Math.max(0, pageCount - 1));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,14 +122,12 @@ export function DataTablePagination<TData>({
|
|||||||
<div className="flex items-center justify-between text-muted-foreground">
|
<div className="flex items-center justify-between text-muted-foreground">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Select
|
<Select
|
||||||
value={`${table.getState().pagination.pageSize}`}
|
value={`${pageSize}`}
|
||||||
onValueChange={handlePageSizeChange}
|
onValueChange={handlePageSizeChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-[73px]" disabled={disabled}>
|
<SelectTrigger className="h-8 w-[73px]" disabled={disabled}>
|
||||||
<SelectValue
|
<SelectValue placeholder={pageSize} />
|
||||||
placeholder={table.getState().pagination.pageSize}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent side="bottom">
|
<SelectContent side="bottom">
|
||||||
{[10, 20, 30, 40, 50, 100].map((pageSize) => (
|
{[10, 20, 30, 40, 50, 100].map((pageSize) => (
|
||||||
@@ -121,16 +143,11 @@ export function DataTablePagination<TData>({
|
|||||||
<div className="flex items-center justify-center text-sm font-medium">
|
<div className="flex items-center justify-center text-sm font-medium">
|
||||||
{isServerPagination && totalCount !== undefined
|
{isServerPagination && totalCount !== undefined
|
||||||
? t("paginator", {
|
? t("paginator", {
|
||||||
current:
|
current: pageIndex + 1,
|
||||||
table.getState().pagination.pageIndex + 1,
|
last: Math.ceil(totalCount / pageSize)
|
||||||
last: Math.ceil(
|
|
||||||
totalCount /
|
|
||||||
table.getState().pagination.pageSize
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
: t("paginator", {
|
: t("paginator", {
|
||||||
current:
|
current: pageIndex + 1,
|
||||||
table.getState().pagination.pageIndex + 1,
|
|
||||||
last: table.getPageCount()
|
last: table.getPageCount()
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -139,9 +156,7 @@ export function DataTablePagination<TData>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => handlePageNavigation("first")}
|
onClick={() => handlePageNavigation("first")}
|
||||||
disabled={
|
disabled={!canPreviousPage || isLoading || disabled}
|
||||||
!table.getCanPreviousPage() || isLoading || disabled
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t("paginatorToFirst")}</span>
|
<span className="sr-only">{t("paginatorToFirst")}</span>
|
||||||
<DoubleArrowLeftIcon className="h-4 w-4" />
|
<DoubleArrowLeftIcon className="h-4 w-4" />
|
||||||
@@ -150,9 +165,7 @@ export function DataTablePagination<TData>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
onClick={() => handlePageNavigation("previous")}
|
onClick={() => handlePageNavigation("previous")}
|
||||||
disabled={
|
disabled={!canPreviousPage || isLoading || disabled}
|
||||||
!table.getCanPreviousPage() || isLoading || disabled
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
{t("paginatorToPrevious")}
|
{t("paginatorToPrevious")}
|
||||||
@@ -163,9 +176,7 @@ export function DataTablePagination<TData>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
onClick={() => handlePageNavigation("next")}
|
onClick={() => handlePageNavigation("next")}
|
||||||
disabled={
|
disabled={!canNextPage || isLoading || disabled}
|
||||||
!table.getCanNextPage() || isLoading || disabled
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t("paginatorToNext")}</span>
|
<span className="sr-only">{t("paginatorToNext")}</span>
|
||||||
<ChevronRightIcon className="h-4 w-4" />
|
<ChevronRightIcon className="h-4 w-4" />
|
||||||
@@ -174,9 +185,7 @@ export function DataTablePagination<TData>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => handlePageNavigation("last")}
|
onClick={() => handlePageNavigation("last")}
|
||||||
disabled={
|
disabled={!canNextPage || isLoading || disabled}
|
||||||
!table.getCanNextPage() || isLoading || disabled
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t("paginatorToLast")}</span>
|
<span className="sr-only">{t("paginatorToLast")}</span>
|
||||||
<DoubleArrowRightIcon className="h-4 w-4" />
|
<DoubleArrowRightIcon className="h-4 w-4" />
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
setSelectedDomain(null);
|
setSelectedDomain(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("domainQuestionRemove")}</p>
|
<p>{t("domainQuestionRemove")}</p>
|
||||||
<p>{t("domainMessageRemove")}</p>
|
<p>{t("domainMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export default function InvitationsTable({
|
|||||||
setSelectedInvitation(null);
|
setSelectedInvitation(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("inviteQuestionRemove")}</p>
|
<p>{t("inviteQuestionRemove")}</p>
|
||||||
<p>{t("inviteMessageRemove")}</p>
|
<p>{t("inviteMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { FaGithub } from "react-icons/fa";
|
import { FaGithub } from "react-icons/fa";
|
||||||
import SidebarLicenseButton from "./SidebarLicenseButton";
|
import SidebarLicenseButton from "./SidebarLicenseButton";
|
||||||
import { SidebarSupportButton } from "./SidebarSupportButton";
|
import { SidebarSupportButton } from "./SidebarSupportButton";
|
||||||
|
import { is } from "drizzle-orm";
|
||||||
|
|
||||||
const ProductUpdates = dynamic(() => import("./ProductUpdates"), {
|
const ProductUpdates = dynamic(() => import("./ProductUpdates"), {
|
||||||
ssr: false
|
ssr: false
|
||||||
@@ -52,7 +53,7 @@ export function LayoutSidebar({
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isAdminPage = pathname?.startsWith("/admin");
|
const isAdminPage = pathname?.startsWith("/admin");
|
||||||
const { user } = useUserContext();
|
const { user } = useUserContext();
|
||||||
const { isUnlocked } = useLicenseStatusContext();
|
const { isUnlocked, licenseStatus } = useLicenseStatusContext();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -226,6 +227,18 @@ export function LayoutSidebar({
|
|||||||
<FaGithub size={12} />
|
<FaGithub size={12} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
{build === "enterprise" &&
|
||||||
|
isUnlocked() &&
|
||||||
|
licenseStatus?.tier === "personal" ? (
|
||||||
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
{t("personalUseOnly")}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{build === "enterprise" && !isUnlocked() ? (
|
||||||
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
|
{t("unlicensed")}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{env?.app?.version && (
|
{env?.app?.version && (
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
<div className="text-xs text-muted-foreground text-center">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -542,6 +542,8 @@ export function LogDataTable<TData, TValue>({
|
|||||||
isServerPagination={isServerPagination}
|
isServerPagination={isServerPagination}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
pageSize={pageSize}
|
||||||
|
pageIndex={currentPage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export default function MachineClientsTable({
|
|||||||
setSelectedClient(null);
|
setSelectedClient(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("deleteClientQuestion")}</p>
|
<p>{t("deleteClientQuestion")}</p>
|
||||||
<p>{t("clientMessageRemove")}</p>
|
<p>{t("clientMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export default function OrgApiKeysTable({
|
|||||||
setSelected(null);
|
setSelected(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("apiKeysQuestionRemove")}</p>
|
<p>{t("apiKeysQuestionRemove")}</p>
|
||||||
|
|
||||||
<p>{t("apiKeysMessageRemove")}</p>
|
<p>{t("apiKeysMessageRemove")}</p>
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ export default function ProxyResourcesTable({
|
|||||||
setSelectedResource(null);
|
setSelectedResource(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("resourceQuestionRemove")}</p>
|
<p>{t("resourceQuestionRemove")}</p>
|
||||||
<p>{t("resourceMessageRemove")}</p>
|
<p>{t("resourceMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ type ResourceAuthPortalProps = {
|
|||||||
export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isUnlocked } = useLicenseStatusContext();
|
const { isUnlocked, licenseStatus } = useLicenseStatusContext();
|
||||||
|
|
||||||
const getNumMethods = () => {
|
const getNumMethods = () => {
|
||||||
let colLength = 0;
|
let colLength = 0;
|
||||||
@@ -737,6 +737,22 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{build === "enterprise" && !isUnlocked() ? (
|
||||||
|
<div className="text-center mt-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t("instanceIsUnlicensed")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{build === "enterprise" &&
|
||||||
|
isUnlocked() &&
|
||||||
|
licenseStatus?.tier === "personal" ? (
|
||||||
|
<div className="text-center mt-2">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t("loginPageLicenseWatermark")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ResourceAccessDenied />
|
<ResourceAccessDenied />
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||||||
setSelectedSite(null);
|
setSelectedSite(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div className="">
|
<div className="space-y-2">
|
||||||
<p>{t("siteQuestionRemove")}</p>
|
<p>{t("siteQuestionRemove")}</p>
|
||||||
<p>{t("siteMessageRemove")}</p>
|
<p>{t("siteMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
setSelectedClient(null);
|
setSelectedClient(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("deleteClientQuestion")}</p>
|
<p>{t("deleteClientQuestion")}</p>
|
||||||
<p>{t("clientMessageRemove")}</p>
|
<p>{t("clientMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
setSelectedUser(null);
|
setSelectedUser(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("userQuestionOrgRemove")}</p>
|
<p>{t("userQuestionOrgRemove")}</p>
|
||||||
<p>{t("userMessageOrgRemove")}</p>
|
<p>{t("userMessageOrgRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export default function ViewDevicesDialog({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>
|
<p>
|
||||||
{t("deviceQuestionRemove") ||
|
{t("deviceQuestionRemove") ||
|
||||||
"Are you sure you want to delete this device?"}
|
"Are you sure you want to delete this device?"}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export default function IdpTable({ idps, orgId }: Props) {
|
|||||||
setSelectedIdp(null);
|
setSelectedIdp(null);
|
||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p>{t("idpQuestionRemove")}</p>
|
<p>{t("idpQuestionRemove")}</p>
|
||||||
<p>{t("idpMessageRemove")}</p>
|
<p>{t("idpMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
getFilteredRowModel,
|
getFilteredRowModel,
|
||||||
VisibilityState
|
VisibilityState,
|
||||||
|
PaginationState
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
// Extended ColumnDef type that includes optional friendlyName for column visibility dropdown
|
// Extended ColumnDef type that includes optional friendlyName for column visibility dropdown
|
||||||
@@ -227,6 +228,10 @@ export function DataTable<TData, TValue>({
|
|||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||||
initialColumnVisibility
|
initialColumnVisibility
|
||||||
);
|
);
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: pageSize
|
||||||
|
});
|
||||||
const [activeTab, setActiveTab] = useState<string>(
|
const [activeTab, setActiveTab] = useState<string>(
|
||||||
defaultTab || tabs?.[0]?.id || ""
|
defaultTab || tabs?.[0]?.id || ""
|
||||||
);
|
);
|
||||||
@@ -256,6 +261,7 @@ export function DataTable<TData, TValue>({
|
|||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
initialState: {
|
initialState: {
|
||||||
pagination: {
|
pagination: {
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
@@ -267,21 +273,18 @@ export function DataTable<TData, TValue>({
|
|||||||
sorting,
|
sorting,
|
||||||
columnFilters,
|
columnFilters,
|
||||||
globalFilter,
|
globalFilter,
|
||||||
columnVisibility
|
columnVisibility,
|
||||||
|
pagination
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Persist pageSize to localStorage when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentPageSize = table.getState().pagination.pageSize;
|
if (persistPageSize && pagination.pageSize !== pageSize) {
|
||||||
if (currentPageSize !== pageSize) {
|
setStoredPageSize(pagination.pageSize, tableId);
|
||||||
table.setPageSize(pageSize);
|
setPageSize(pagination.pageSize);
|
||||||
|
|
||||||
// Persist to localStorage if enabled
|
|
||||||
if (persistPageSize) {
|
|
||||||
setStoredPageSize(pageSize, tableId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [pageSize, table, persistPageSize, tableId]);
|
}, [pagination.pageSize, persistPageSize, tableId, pageSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Persist column visibility to localStorage when it changes
|
// Persist column visibility to localStorage when it changes
|
||||||
@@ -293,13 +296,17 @@ export function DataTable<TData, TValue>({
|
|||||||
const handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
setActiveTab(value);
|
setActiveTab(value);
|
||||||
// Reset to first page when changing tabs
|
// Reset to first page when changing tabs
|
||||||
table.setPageIndex(0);
|
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enhanced pagination component that updates our local state
|
// Enhanced pagination component that updates our local state
|
||||||
const handlePageSizeChange = (newPageSize: number) => {
|
const handlePageSizeChange = (newPageSize: number) => {
|
||||||
|
setPagination((prev) => ({
|
||||||
|
...prev,
|
||||||
|
pageSize: newPageSize,
|
||||||
|
pageIndex: 0
|
||||||
|
}));
|
||||||
setPageSize(newPageSize);
|
setPageSize(newPageSize);
|
||||||
table.setPageSize(newPageSize);
|
|
||||||
|
|
||||||
// Persist immediately when changed
|
// Persist immediately when changed
|
||||||
if (persistPageSize) {
|
if (persistPageSize) {
|
||||||
@@ -614,6 +621,8 @@ export function DataTable<TData, TValue>({
|
|||||||
<DataTablePagination
|
<DataTablePagination
|
||||||
table={table}
|
table={table}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
|
pageSize={pagination.pageSize}
|
||||||
|
pageIndex={pagination.pageIndex}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user