mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-03 09:16:40 +00:00
Merge branch 'dev' into refactor/save-button-positions
This commit is contained in:
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
|
||||||
|
|
||||||
|
|||||||
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:
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func installDocker() error {
|
|||||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
apt-get install -y apt-transport-https ca-certificates curl gpg &&
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
@@ -82,7 +82,7 @@ func installDocker() error {
|
|||||||
case strings.Contains(osRelease, "ID=debian"):
|
case strings.Contains(osRelease, "ID=debian"):
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
apt-get install -y apt-transport-https ca-certificates curl gpg &&
|
||||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
|
|||||||
@@ -2275,5 +2275,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."
|
||||||
}
|
}
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -90,7 +90,7 @@
|
|||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.3",
|
"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",
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.3",
|
"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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -432,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()]);
|
||||||
@@ -456,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) {
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ export async function getTraefikConfig(
|
|||||||
(cert) => cert.queriedDomain === lp.fullDomain
|
(cert) => cert.queriedDomain === lp.fullDomain
|
||||||
);
|
);
|
||||||
if (!matchingCert) {
|
if (!matchingCert) {
|
||||||
logger.warn(
|
logger.debug(
|
||||||
`No matching certificate found for login page domain: ${lp.fullDomain}`
|
`No matching certificate found for login page domain: ${lp.fullDomain}`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ export async function getConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exitNode = await createExitNode(publicKey, reachableAt);
|
// clean up the public key - keep only valid base64 characters (A-Z, a-z, 0-9, +, /, =)
|
||||||
|
const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, '');
|
||||||
|
|
||||||
|
const exitNode = await createExitNode(cleanedPublicKey, reachableAt);
|
||||||
|
|
||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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