mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-17 21:01:53 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e271028f3 | ||
|
|
820f66e58f | ||
|
|
b0fdc10e06 | ||
|
|
b82b41ed26 | ||
|
|
3e977ba00d | ||
|
|
a724b07846 | ||
|
|
5f0bc71bcd | ||
|
|
aea7827c1a | ||
|
|
d865c4c55b | ||
|
|
cfe33eb974 | ||
|
|
3cc244a1d3 | ||
|
|
10542d7282 | ||
|
|
7fa1180d10 | ||
|
|
8b50f1fb65 | ||
|
|
527d4cc777 |
@@ -34,4 +34,5 @@ build.ts
|
|||||||
tsconfig.json
|
tsconfig.json
|
||||||
Dockerfile*
|
Dockerfile*
|
||||||
drizzle.config.ts
|
drizzle.config.ts
|
||||||
allowedDevOrigins.json
|
allowedDevOrigins.json
|
||||||
|
scratch/
|
||||||
|
|||||||
@@ -984,7 +984,7 @@
|
|||||||
"sharedPolicy": "Shared Policy",
|
"sharedPolicy": "Shared Policy",
|
||||||
"sharedPolicyNoneDescription": "This resource has its own policy.",
|
"sharedPolicyNoneDescription": "This resource has its own policy.",
|
||||||
"resourceSharedPolicyOwnDescription": "This resource has its own authentication and access rules controls.",
|
"resourceSharedPolicyOwnDescription": "This resource has its own authentication and access rules controls.",
|
||||||
"resourceSharedPolicyInheritedDescription": "This resource inherits authentication and access rules controls from <policyLink>{policyName}</policyLink>.",
|
"resourceSharedPolicyInheritedDescription": "This resource inherits from <policyLink>{policyName}</policyLink>.",
|
||||||
"resourceSharedPolicyAuthenticationNotice": "This resource is using a shared policy. Some authentication settings can be edited on this resource to add to the policy. To change the underlying policy, you must edit to <policyLink>{policyName}</policyLink>.",
|
"resourceSharedPolicyAuthenticationNotice": "This resource is using a shared policy. Some authentication settings can be edited on this resource to add to the policy. To change the underlying policy, you must edit to <policyLink>{policyName}</policyLink>.",
|
||||||
"resourceSharedPolicyRulesNotice": "This resource is using a shared policy. Some access rules can be edited on this resource. To change the underlying policy, you must edit <policyLink>{policyName}</policyLink>.",
|
"resourceSharedPolicyRulesNotice": "This resource is using a shared policy. Some access rules can be edited on this resource. To change the underlying policy, you must edit <policyLink>{policyName}</policyLink>.",
|
||||||
"resourceUsersRoles": "Access Controls",
|
"resourceUsersRoles": "Access Controls",
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import license from "#dynamic/license/license";
|
|||||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||||
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
||||||
import { fetchServerIp } from "@server/lib/serverIpService";
|
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||||
import { startRebuildQueueProcessor } from "@server/lib/rebuildClientAssociations";
|
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
@@ -42,7 +41,6 @@ async function startServers() {
|
|||||||
|
|
||||||
initLogCleanupInterval();
|
initLogCleanupInterval();
|
||||||
initAcmeCertSync();
|
initAcmeCertSync();
|
||||||
startRebuildQueueProcessor();
|
|
||||||
|
|
||||||
// Start all servers
|
// Start all servers
|
||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
exitNodes,
|
exitNodes,
|
||||||
newts,
|
newts,
|
||||||
olms,
|
olms,
|
||||||
primaryDb,
|
|
||||||
roleSiteResources,
|
roleSiteResources,
|
||||||
Site,
|
Site,
|
||||||
SiteResource,
|
SiteResource,
|
||||||
@@ -41,7 +40,6 @@ import {
|
|||||||
removeTargets as removeSubnetProxyTargets
|
removeTargets as removeSubnetProxyTargets
|
||||||
} from "@server/routers/client/targets";
|
} from "@server/routers/client/targets";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
import { rebuildQueue } from "#dynamic/lib/rebuildQueue";
|
|
||||||
|
|
||||||
// TTL for rebuild-association locks. These functions can fan out into many
|
// TTL for rebuild-association locks. These functions can fan out into many
|
||||||
// peer/proxy updates, so give them a generous window.
|
// peer/proxy updates, so give them a generous window.
|
||||||
@@ -169,32 +167,11 @@ export async function rebuildClientAssociationsFromSiteResource(
|
|||||||
subnet: string | null;
|
subnet: string | null;
|
||||||
}[];
|
}[];
|
||||||
}> {
|
}> {
|
||||||
try {
|
return await lockManager.withLock(
|
||||||
return await lockManager.withLock(
|
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
||||||
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
() => rebuildClientAssociationsFromSiteResourceImpl(siteResource, trx),
|
||||||
() =>
|
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||||
rebuildClientAssociationsFromSiteResourceImpl(
|
);
|
||||||
siteResource,
|
|
||||||
trx
|
|
||||||
),
|
|
||||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
if (
|
|
||||||
typeof err?.message === "string" &&
|
|
||||||
err.message.startsWith("Failed to acquire lock")
|
|
||||||
) {
|
|
||||||
logger.warn(
|
|
||||||
`rebuildClientAssociations: could not acquire lock for site resource ${siteResource.siteResourceId}, queuing for deferred processing`
|
|
||||||
);
|
|
||||||
await rebuildQueue.enqueue({
|
|
||||||
type: "site-resource",
|
|
||||||
id: siteResource.siteResourceId
|
|
||||||
});
|
|
||||||
return { mergedAllClients: [] };
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebuildClientAssociationsFromSiteResourceImpl(
|
async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||||
@@ -979,28 +956,11 @@ export async function rebuildClientAssociationsFromClient(
|
|||||||
client: Client,
|
client: Client,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
return await lockManager.withLock(
|
||||||
return await lockManager.withLock(
|
`rebuild-client-associations:client:${client.clientId}`,
|
||||||
`rebuild-client-associations:client:${client.clientId}`,
|
() => rebuildClientAssociationsFromClientImpl(client, trx),
|
||||||
() => rebuildClientAssociationsFromClientImpl(client, trx),
|
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
);
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
if (
|
|
||||||
typeof err?.message === "string" &&
|
|
||||||
err.message.startsWith("Failed to acquire lock")
|
|
||||||
) {
|
|
||||||
logger.warn(
|
|
||||||
`rebuildClientAssociations: could not acquire lock for client ${client.clientId}, queuing for deferred processing`
|
|
||||||
);
|
|
||||||
await rebuildQueue.enqueue({
|
|
||||||
type: "client",
|
|
||||||
id: client.clientId
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebuildClientAssociationsFromClientImpl(
|
async function rebuildClientAssociationsFromClientImpl(
|
||||||
@@ -1946,47 +1906,3 @@ export async function cleanupSiteAssociations(
|
|||||||
|
|
||||||
logger.debug(`cleanupSiteAssociations: DONE siteId=${siteId}`);
|
logger.debug(`cleanupSiteAssociations: DONE siteId=${siteId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the background rebuild queue processor. This should be called once
|
|
||||||
* during server startup. Only one server instance at a time will actively
|
|
||||||
* consume the queue (enforced via a distributed Redis lock); all other
|
|
||||||
* instances will poll and wait until the lock becomes available.
|
|
||||||
*/
|
|
||||||
export function startRebuildQueueProcessor(): void {
|
|
||||||
rebuildQueue.startProcessing({
|
|
||||||
onSiteResource: async (siteResourceId: number) => {
|
|
||||||
const [siteResource] = await primaryDb
|
|
||||||
.select()
|
|
||||||
.from(siteResources)
|
|
||||||
.where(eq(siteResources.siteResourceId, siteResourceId));
|
|
||||||
|
|
||||||
if (!siteResource) {
|
|
||||||
logger.warn(
|
|
||||||
`Rebuild queue: site resource ${siteResourceId} not found, skipping`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
|
||||||
siteResource,
|
|
||||||
primaryDb
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onClient: async (clientId: number) => {
|
|
||||||
const [client] = await primaryDb
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.clientId, clientId));
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
logger.warn(
|
|
||||||
`Rebuild queue: client ${clientId} not found, skipping`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(client, primaryDb);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
export type RebuildJobType = "site-resource" | "client";
|
|
||||||
|
|
||||||
export interface RebuildJob {
|
|
||||||
type: RebuildJobType;
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RebuildJobHandlers {
|
|
||||||
onSiteResource(siteResourceId: number): Promise<void>;
|
|
||||||
onClient(clientId: number): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RebuildQueueManager {
|
|
||||||
enqueue(job: RebuildJob): Promise<void>;
|
|
||||||
startProcessing(handlers: RebuildJobHandlers): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoopRebuildQueue implements RebuildQueueManager {
|
|
||||||
async enqueue(_job: RebuildJob): Promise<void> {}
|
|
||||||
startProcessing(_handlers: RebuildJobHandlers): void {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rebuildQueue: RebuildQueueManager = new NoopRebuildQueue();
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of a proprietary work.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This file is licensed under the Fossorial Commercial License.
|
|
||||||
* You may not use this file except in compliance with the License.
|
|
||||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
|
||||||
*
|
|
||||||
* This file is not licensed under the AGPLv3.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { redis } from "#private/lib/redis";
|
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export type RebuildJobType = "site-resource" | "client";
|
|
||||||
|
|
||||||
export interface RebuildJob {
|
|
||||||
type: RebuildJobType;
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RebuildJobHandlers {
|
|
||||||
onSiteResource(siteResourceId: number): Promise<void>;
|
|
||||||
onClient(clientId: number): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redis list holding pending rebuild jobs (RPUSH to enqueue, LPOP to dequeue — FIFO order).
|
|
||||||
const QUEUE_KEY = "rebuild-client-associations:queue";
|
|
||||||
|
|
||||||
// Distributed lock that serialises queue consumption to a single server instance
|
|
||||||
// at a time. TTL is generous enough to cover a full batch of expensive rebuilds.
|
|
||||||
const PROCESSOR_LOCK_KEY = "rebuild-client-associations:processor";
|
|
||||||
|
|
||||||
// Each rebuild can take up to REBUILD_ASSOCIATIONS_LOCK_TTL_MS (120 s) per
|
|
||||||
// resource. Allow BATCH_SIZE resources per processor-lock acquisition, plus a
|
|
||||||
// small buffer.
|
|
||||||
const BATCH_SIZE = 5;
|
|
||||||
const PROCESSOR_LOCK_TTL_MS = 120000 * BATCH_SIZE + 30000; // ~630 s
|
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 500;
|
|
||||||
|
|
||||||
class RedisRebuildQueue {
|
|
||||||
private processingStarted = false;
|
|
||||||
|
|
||||||
async enqueue(job: RebuildJob): Promise<void> {
|
|
||||||
if (!redis || redis.status !== "ready") {
|
|
||||||
logger.warn(
|
|
||||||
`Rebuild queue: Redis not available — rebuild for ${job.type}:${job.id} will not be retried`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await redis.rpush(QUEUE_KEY, JSON.stringify(job));
|
|
||||||
logger.debug(
|
|
||||||
`Rebuild queue: enqueued ${job.type}:${job.id} (queue position: tail)`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
`Rebuild queue: failed to enqueue ${job.type}:${job.id}:`,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startProcessing(handlers: RebuildJobHandlers): void {
|
|
||||||
if (this.processingStarted) return;
|
|
||||||
this.processingStarted = true;
|
|
||||||
|
|
||||||
this.processLoop(handlers).catch((err) => {
|
|
||||||
logger.error("Rebuild queue processor loop crashed:", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info("Rebuild queue processor started");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processLoop(handlers: RebuildJobHandlers): Promise<void> {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
await this.tryProcessBatch(handlers);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
"Rebuild queue: unhandled error in process loop:",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, POLL_INTERVAL_MS)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async tryProcessBatch(handlers: RebuildJobHandlers): Promise<void> {
|
|
||||||
if (!redis || redis.status !== "ready") return;
|
|
||||||
|
|
||||||
// Peek before acquiring the processor lock to avoid unnecessary Redis
|
|
||||||
// round-trips and lock contention when the queue is idle.
|
|
||||||
const queueLength = await redis.llen(QUEUE_KEY).catch(() => 0);
|
|
||||||
if (queueLength === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await lockManager.withLock(
|
|
||||||
PROCESSOR_LOCK_KEY,
|
|
||||||
async () => {
|
|
||||||
for (let i = 0; i < BATCH_SIZE; i++) {
|
|
||||||
if (!redis || redis.status !== "ready") break;
|
|
||||||
|
|
||||||
const payload = await redis.lpop(QUEUE_KEY);
|
|
||||||
if (payload === null) break; // queue drained
|
|
||||||
|
|
||||||
let job: RebuildJob;
|
|
||||||
try {
|
|
||||||
job = JSON.parse(payload) as RebuildJob;
|
|
||||||
} catch {
|
|
||||||
logger.error(
|
|
||||||
`Rebuild queue: could not parse job payload, discarding: ${payload}`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Rebuild queue: processing ${job.type}:${job.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (job.type === "site-resource") {
|
|
||||||
await handlers.onSiteResource(job.id);
|
|
||||||
} else if (job.type === "client") {
|
|
||||||
await handlers.onClient(job.id);
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
`Rebuild queue: unknown job type "${(job as any).type}", discarding`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`Rebuild queue: completed ${job.type}:${job.id}`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
`Rebuild queue: job ${job.type}:${job.id} threw an error:`,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PROCESSOR_LOCK_TTL_MS
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
if (
|
|
||||||
typeof err?.message === "string" &&
|
|
||||||
err.message.startsWith("Failed to acquire lock")
|
|
||||||
) {
|
|
||||||
// Another server instance currently holds the processor lock and
|
|
||||||
// is consuming the queue — nothing to do this cycle.
|
|
||||||
logger.debug(
|
|
||||||
"Rebuild queue: processor lock held by another instance, skipping this cycle"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rebuildQueue: RedisRebuildQueue = new RedisRebuildQueue();
|
|
||||||
@@ -228,7 +228,7 @@ export default async function migration() {
|
|||||||
).run();
|
).run();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
UPDATE 'siteResources' SET 'destination2' = 'destination';
|
UPDATE 'siteResources' SET "destination2" = "destination";
|
||||||
`
|
`
|
||||||
).run();
|
).run();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
@@ -349,9 +349,9 @@ export default async function migration() {
|
|||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
UPDATE 'targets'
|
UPDATE 'targets'
|
||||||
SET 'mode' = (
|
SET "mode" = (
|
||||||
SELECT 'mode' FROM 'resources'
|
SELECT "mode" FROM 'resources'
|
||||||
WHERE 'resources'.'resourceId' = 'targets'.'resourceId'
|
WHERE "resources"."resourceId" = "targets"."resourceId"
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
).run();
|
).run();
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { authCookieHeader } from "@app/lib/api/cookies";
|
|||||||
import { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
import { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import OrgLabelsTable from "@app/components/OrgLabelsTable";
|
import OrgLabelsTable from "@app/components/OrgLabelsTable";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
@@ -49,6 +51,8 @@ export default async function LabelsPage({ params, searchParams }: Props) {
|
|||||||
description={t("orgLabelsDescription")}
|
description={t("orgLabelsDescription")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
||||||
|
|
||||||
<OrgLabelsTable
|
<OrgLabelsTable
|
||||||
labels={labels}
|
labels={labels}
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|||||||
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { env } from "process";
|
||||||
|
|
||||||
// Schema for general organization settings
|
// Schema for general organization settings
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
@@ -165,6 +166,7 @@ function DeleteForm({ org }: SectionFormProps) {
|
|||||||
|
|
||||||
function GeneralSectionForm({ org }: SectionFormProps) {
|
function GeneralSectionForm({ org }: SectionFormProps) {
|
||||||
const { updateOrg } = useOrgContext();
|
const { updateOrg } = useOrgContext();
|
||||||
|
const { env } = useEnvContext();
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
GeneralFormSchema.pick({
|
GeneralFormSchema.pick({
|
||||||
@@ -265,36 +267,42 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
<PaidFeaturesAlert
|
<PaidFeaturesAlert
|
||||||
tiers={tierMatrix.newtAutoUpdate}
|
tiers={tierMatrix.newtAutoUpdate}
|
||||||
/>
|
/>
|
||||||
<FormField
|
{!env.flags.disableEnterpriseFeatures && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="settingsEnableGlobalNewtAutoUpdate"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="settingsEnableGlobalNewtAutoUpdate"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormControl>
|
<FormItem>
|
||||||
<SwitchInput
|
<FormControl>
|
||||||
id="settings-enable-global-newt-auto-update"
|
<SwitchInput
|
||||||
label={t("newtAutoUpdate")}
|
id="settings-enable-global-newt-auto-update"
|
||||||
checked={field.value}
|
label={t("newtAutoUpdate")}
|
||||||
onCheckedChange={field.onChange}
|
checked={field.value}
|
||||||
disabled={!hasAutoUpdateFeature}
|
onCheckedChange={
|
||||||
/>
|
field.onChange
|
||||||
</FormControl>
|
}
|
||||||
<FormDescription>
|
disabled={
|
||||||
{t("newtAutoUpdateDescription")}{" "}
|
!hasAutoUpdateFeature
|
||||||
<a
|
}
|
||||||
href="https://docs.pangolin.net/manage/sites/auto-update"
|
/>
|
||||||
target="_blank"
|
</FormControl>
|
||||||
rel="noopener noreferrer"
|
<FormDescription>
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
{t("newtAutoUpdateDescription")}{" "}
|
||||||
>
|
<a
|
||||||
{t("learnMore")}
|
href="https://docs.pangolin.net/manage/sites/auto-update"
|
||||||
<ExternalLink className="size-3.5 shrink-0" />
|
target="_blank"
|
||||||
</a>
|
rel="noopener noreferrer"
|
||||||
</FormDescription>
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
<FormMessage />
|
>
|
||||||
</FormItem>
|
{t("learnMore")}
|
||||||
)}
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
/>
|
</a>
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ import {
|
|||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionFooter,
|
SettingsSectionFooter,
|
||||||
SettingsFormCell,
|
|
||||||
SettingsFormGrid,
|
SettingsFormGrid,
|
||||||
SettingsSectionForm,
|
SettingsSectionForm,
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle,
|
SettingsSectionTitle,
|
||||||
SettingsSubsectionDescription,
|
SettingsSubsectionDescription,
|
||||||
SettingsSubsectionHeader,
|
SettingsSubsectionHeader,
|
||||||
SettingsSubsectionTitle
|
SettingsSubsectionTitle,
|
||||||
|
SettingsFormCell
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
@@ -70,7 +70,7 @@ export default function GeneralForm() {
|
|||||||
|
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
|
|
||||||
const showResourcePolicy =
|
const hasResourcePolicies =
|
||||||
build !== "oss" &&
|
build !== "oss" &&
|
||||||
isPaidUser(tierMatrix[TierFeature.ResourcePolicies]);
|
isPaidUser(tierMatrix[TierFeature.ResourcePolicies]);
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export default function GeneralForm() {
|
|||||||
...orgQueries.resourcePolicy({
|
...orgQueries.resourcePolicy({
|
||||||
resourcePolicyId: selectedSharedPolicyId!
|
resourcePolicyId: selectedSharedPolicyId!
|
||||||
}),
|
}),
|
||||||
enabled: showResourcePolicy && selectedSharedPolicyId !== null
|
enabled: hasResourcePolicies && selectedSharedPolicyId !== null
|
||||||
});
|
});
|
||||||
|
|
||||||
const [resourceFullDomain, setResourceFullDomain] = useState(
|
const [resourceFullDomain, setResourceFullDomain] = useState(
|
||||||
@@ -153,11 +153,10 @@ export default function GeneralForm() {
|
|||||||
|
|
||||||
let resourcePolicyId: number | null | undefined;
|
let resourcePolicyId: number | null | undefined;
|
||||||
|
|
||||||
if (
|
if (!["tcp", "udp"].includes(resource.mode)) {
|
||||||
showResourcePolicy &&
|
if (hasResourcePolicies || selectedSharedPolicyId === null) {
|
||||||
!["tcp", "udp"].includes(resource.mode)
|
resourcePolicyId = selectedSharedPolicyId;
|
||||||
) {
|
}
|
||||||
resourcePolicyId = selectedSharedPolicyId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -297,28 +296,6 @@ export default function GeneralForm() {
|
|||||||
/>
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
|
|
||||||
<SettingsFormCell span="full">
|
|
||||||
<SettingsSubsectionHeader>
|
|
||||||
<SettingsSubsectionTitle>
|
|
||||||
{t(
|
|
||||||
"resourceGeneralDetailsSubsection"
|
|
||||||
)}
|
|
||||||
</SettingsSubsectionTitle>
|
|
||||||
<SettingsSubsectionDescription>
|
|
||||||
{t(
|
|
||||||
[
|
|
||||||
"tcp",
|
|
||||||
"udp",
|
|
||||||
].includes(
|
|
||||||
resource.mode
|
|
||||||
)
|
|
||||||
? "resourceGeneralDetailsSubsectionPortDescription"
|
|
||||||
: "resourceGeneralDetailsSubsectionDescription"
|
|
||||||
)}
|
|
||||||
</SettingsSubsectionDescription>
|
|
||||||
</SettingsSubsectionHeader>
|
|
||||||
</SettingsFormCell>
|
|
||||||
|
|
||||||
<SettingsFormCell span="half">
|
<SettingsFormCell span="half">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -476,10 +453,9 @@ export default function GeneralForm() {
|
|||||||
</div>
|
</div>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
)}
|
)}
|
||||||
{showResourcePolicy &&
|
{ !["tcp", "udp"].includes(
|
||||||
!["tcp", "udp"].includes(
|
|
||||||
resource.mode
|
resource.mode
|
||||||
) && (
|
) && !env.flags.disableEnterpriseFeatures && (
|
||||||
<>
|
<>
|
||||||
<SettingsFormCell span="full">
|
<SettingsFormCell span="full">
|
||||||
<SettingsSubsectionHeader>
|
<SettingsSubsectionHeader>
|
||||||
|
|||||||
@@ -169,20 +169,27 @@ export default function ResourceMaintenancePage() {
|
|||||||
{
|
{
|
||||||
id: "automatic",
|
id: "automatic",
|
||||||
title: `${t("automatic")} (${t("recommended")})`,
|
title: `${t("automatic")} (${t("recommended")})`,
|
||||||
description: t("automaticModeDescription"),
|
description: t("automaticModeDescription")
|
||||||
disabled: isMaintenanceDisabled
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "forced",
|
id: "forced",
|
||||||
title: t("forced"),
|
title: t("forced"),
|
||||||
description: t("forcedModeDescription"),
|
description: t("forcedModeDescription")
|
||||||
disabled: isMaintenanceDisabled
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<>
|
||||||
<SettingsSection>
|
<PaidFeaturesAlert tiers={tierMatrix.maintencePage} />
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isMaintenanceDisabled
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsContainer>
|
||||||
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("maintenanceMode")}
|
{t("maintenanceMode")}
|
||||||
@@ -193,7 +200,6 @@ export default function ResourceMaintenancePage() {
|
|||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.maintencePage} />
|
|
||||||
<SettingsSectionForm variant="half">
|
<SettingsSectionForm variant="half">
|
||||||
<Form {...maintenanceForm}>
|
<Form {...maintenanceForm}>
|
||||||
<form
|
<form
|
||||||
@@ -205,46 +211,33 @@ export default function ResourceMaintenancePage() {
|
|||||||
<FormField
|
<FormField
|
||||||
control={maintenanceForm.control}
|
control={maintenanceForm.control}
|
||||||
name="maintenanceModeEnabled"
|
name="maintenanceModeEnabled"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
const isDisabled = !isPaidUser(
|
<FormItem>
|
||||||
tierMatrix.maintencePage
|
<FormControl>
|
||||||
);
|
<SwitchInput
|
||||||
|
id="enable-maintenance"
|
||||||
return (
|
checked={
|
||||||
<FormItem>
|
field.value
|
||||||
<FormControl>
|
}
|
||||||
<SwitchInput
|
label={t(
|
||||||
id="enable-maintenance"
|
"enableMaintenanceMode"
|
||||||
checked={
|
)}
|
||||||
field.value
|
description={t(
|
||||||
}
|
"enableMaintenanceModeDescription"
|
||||||
label={t(
|
)}
|
||||||
"enableMaintenanceMode"
|
onCheckedChange={(
|
||||||
)}
|
val
|
||||||
description={t(
|
) => {
|
||||||
"enableMaintenanceModeDescription"
|
maintenanceForm.setValue(
|
||||||
)}
|
"maintenanceModeEnabled",
|
||||||
disabled={
|
|
||||||
isDisabled
|
|
||||||
}
|
|
||||||
onCheckedChange={(
|
|
||||||
val
|
val
|
||||||
) => {
|
);
|
||||||
if (
|
}}
|
||||||
!isDisabled
|
/>
|
||||||
) {
|
</FormControl>
|
||||||
maintenanceForm.setValue(
|
<FormMessage />
|
||||||
"maintenanceModeEnabled",
|
</FormItem>
|
||||||
val
|
)}
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
|
|
||||||
@@ -329,11 +322,6 @@ export default function ResourceMaintenancePage() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
disabled={
|
|
||||||
!isPaidUser(
|
|
||||||
tierMatrix.maintencePage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder="We'll be back soon!"
|
placeholder="We'll be back soon!"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -365,11 +353,6 @@ export default function ResourceMaintenancePage() {
|
|||||||
<Textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
rows={4}
|
rows={4}
|
||||||
disabled={
|
|
||||||
!isPaidUser(
|
|
||||||
tierMatrix.maintencePage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"maintenancePageMessagePlaceholder"
|
"maintenancePageMessagePlaceholder"
|
||||||
)}
|
)}
|
||||||
@@ -402,11 +385,6 @@ export default function ResourceMaintenancePage() {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
disabled={
|
|
||||||
!isPaidUser(
|
|
||||||
tierMatrix.maintencePage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
"maintenanceTime"
|
"maintenanceTime"
|
||||||
)}
|
)}
|
||||||
@@ -430,20 +408,19 @@ export default function ResourceMaintenancePage() {
|
|||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
|
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={maintenanceSaveLoading}
|
loading={maintenanceSaveLoading}
|
||||||
disabled={
|
disabled={maintenanceSaveLoading}
|
||||||
maintenanceSaveLoading ||
|
form="maintenance-settings-form"
|
||||||
!isPaidUser(tierMatrix.maintencePage)
|
>
|
||||||
}
|
{t("saveSettings")}
|
||||||
form="maintenance-settings-form"
|
</Button>
|
||||||
>
|
</SettingsSectionFooter>
|
||||||
{t("saveSettings")}
|
</SettingsSection>
|
||||||
</Button>
|
</SettingsContainer>
|
||||||
</SettingsSectionFooter>
|
</div>
|
||||||
</SettingsSection>
|
</>
|
||||||
</SettingsContainer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,85 +253,87 @@ export default function GeneralPage() {
|
|||||||
<PaidFeaturesAlert
|
<PaidFeaturesAlert
|
||||||
tiers={tierMatrix.newtAutoUpdate}
|
tiers={tierMatrix.newtAutoUpdate}
|
||||||
/>
|
/>
|
||||||
{site && site.type === "newt" && (
|
{site &&
|
||||||
<FormField
|
site.type === "newt" &&
|
||||||
control={form.control}
|
!env.flags.disableEnterpriseFeatures && (
|
||||||
name="autoUpdateEnabled"
|
<FormField
|
||||||
render={({ field }) => {
|
control={form.control}
|
||||||
const isOverriding = form.watch(
|
name="autoUpdateEnabled"
|
||||||
"autoUpdateOverrideOrg"
|
render={({ field }) => {
|
||||||
);
|
const isOverriding = form.watch(
|
||||||
return (
|
"autoUpdateOverrideOrg"
|
||||||
<FormItem>
|
);
|
||||||
<FormControl>
|
return (
|
||||||
<div className="">
|
<FormItem>
|
||||||
<SwitchInput
|
<FormControl>
|
||||||
id="auto-update-enabled"
|
<div className="">
|
||||||
label={t(
|
<SwitchInput
|
||||||
"siteAutoUpdateLabel"
|
id="auto-update-enabled"
|
||||||
)}
|
label={t(
|
||||||
checked={
|
"siteAutoUpdateLabel"
|
||||||
field.value
|
)}
|
||||||
}
|
checked={
|
||||||
onCheckedChange={(
|
field.value
|
||||||
checked
|
}
|
||||||
) => {
|
onCheckedChange={(
|
||||||
field.onChange(
|
|
||||||
checked
|
checked
|
||||||
);
|
) => {
|
||||||
form.setValue(
|
field.onChange(
|
||||||
"autoUpdateOverrideOrg",
|
checked
|
||||||
true
|
);
|
||||||
);
|
|
||||||
}}
|
|
||||||
disabled={
|
|
||||||
!hasAutoUpdateFeature
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{isOverriding && (
|
|
||||||
<ButtonUI
|
|
||||||
type="button"
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="text-sm text-muted-foreground px-0"
|
|
||||||
onClick={() => {
|
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"autoUpdateOverrideOrg",
|
"autoUpdateOverrideOrg",
|
||||||
false
|
true
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"autoUpdateEnabled",
|
|
||||||
orgAutoUpdate
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
disabled={
|
||||||
{t(
|
!hasAutoUpdateFeature
|
||||||
"siteAutoUpdateResetToOrg"
|
}
|
||||||
)}
|
/>
|
||||||
</ButtonUI>
|
{isOverriding && (
|
||||||
)}
|
<ButtonUI
|
||||||
</div>
|
type="button"
|
||||||
</FormControl>
|
variant="link"
|
||||||
<FormDescription>
|
size="sm"
|
||||||
{t(
|
className="text-sm text-muted-foreground px-0"
|
||||||
"siteAutoUpdateDescription"
|
onClick={() => {
|
||||||
)}{" "}
|
form.setValue(
|
||||||
<a
|
"autoUpdateOverrideOrg",
|
||||||
href="https://docs.pangolin.net/manage/sites/auto-update"
|
false
|
||||||
target="_blank"
|
);
|
||||||
rel="noopener noreferrer"
|
form.setValue(
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
"autoUpdateEnabled",
|
||||||
>
|
orgAutoUpdate
|
||||||
{t("learnMore")}
|
);
|
||||||
<ExternalLink className="size-3.5 shrink-0" />
|
}}
|
||||||
</a>
|
>
|
||||||
</FormDescription>
|
{t(
|
||||||
<FormMessage />
|
"siteAutoUpdateResetToOrg"
|
||||||
</FormItem>
|
)}
|
||||||
);
|
</ButtonUI>
|
||||||
}}
|
)}
|
||||||
/>
|
</div>
|
||||||
)}
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"siteAutoUpdateDescription"
|
||||||
|
)}{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.pangolin.net/manage/sites/auto-update"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{t("learnMore")}
|
||||||
|
<ExternalLink className="size-3.5 shrink-0" />
|
||||||
|
</a>
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createElement, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
@@ -37,15 +37,6 @@ import {
|
|||||||
InfoSections,
|
InfoSections,
|
||||||
InfoSectionTitle
|
InfoSectionTitle
|
||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
import {
|
|
||||||
FaApple,
|
|
||||||
FaCubes,
|
|
||||||
FaDocker,
|
|
||||||
FaFreebsd,
|
|
||||||
FaWindows
|
|
||||||
} from "react-icons/fa";
|
|
||||||
import { SiNixos, SiKubernetes } from "react-icons/si";
|
|
||||||
import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { generateKeypair } from "../[niceId]/wireguardConfig";
|
import { generateKeypair } from "../[niceId]/wireguardConfig";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
|||||||
@@ -156,10 +156,11 @@ export const orgNavSections = (
|
|||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
// PaidFeaturesAlert
|
// PaidFeaturesAlert
|
||||||
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
|
...(!env?.flags.disableEnterpriseFeatures &&
|
||||||
build === "saas" ||
|
(build === "saas" ||
|
||||||
env?.app.identityProviderMode === "org" ||
|
env?.app.identityProviderMode === "org" ||
|
||||||
(env?.app.identityProviderMode === undefined && build !== "oss")
|
(env?.app.identityProviderMode === undefined &&
|
||||||
|
build !== "oss"))
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
title: "sidebarIdentityProviders",
|
title: "sidebarIdentityProviders",
|
||||||
@@ -259,7 +260,7 @@ export const orgNavSections = (
|
|||||||
href: "/{orgId}/settings/api-keys",
|
href: "/{orgId}/settings/api-keys",
|
||||||
icon: <KeyRound className="size-4 flex-none" />
|
icon: <KeyRound className="size-4 flex-none" />
|
||||||
},
|
},
|
||||||
...(build !== "oss"
|
...(!env?.flags.disableEnterpriseFeatures
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
title: "labels",
|
title: "labels",
|
||||||
|
|||||||
@@ -12,14 +12,7 @@ import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { type PaginationState } from "@tanstack/react-table";
|
import { type PaginationState } from "@tanstack/react-table";
|
||||||
import {
|
import { ArrowRight, MoreHorizontal } from "lucide-react";
|
||||||
ArrowDown01Icon,
|
|
||||||
ArrowUp10Icon,
|
|
||||||
ChevronsUpDownIcon,
|
|
||||||
MoreHorizontal,
|
|
||||||
PencilIcon,
|
|
||||||
PencilLineIcon
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useActionState, useMemo, useState, useTransition } from "react";
|
import { useActionState, useMemo, useState, useTransition } from "react";
|
||||||
@@ -109,7 +102,7 @@ export default function OrgLabelsTable({
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center gap-1.5 group">
|
<div className="flex items-center gap-1.5 group">
|
||||||
<div
|
<div
|
||||||
className="size-2.5 rounded-full bg-(--color) flex-none"
|
className="size-2 rounded-full bg-(--color) flex-none"
|
||||||
style={{
|
style={{
|
||||||
// @ts-expect-error css color
|
// @ts-expect-error css color
|
||||||
"--color": row.original.color
|
"--color": row.original.color
|
||||||
@@ -125,34 +118,40 @@ export default function OrgLabelsTable({
|
|||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
header: () => <span className="p-3"></span>,
|
header: () => <span className="p-3"></span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<DropdownMenu>
|
<div className="flex items-center gap-2 justify-end">
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<DropdownMenuTrigger asChild>
|
||||||
<span className="sr-only">{t("openMenu")}</span>
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<span className="sr-only">
|
||||||
</Button>
|
{t("openMenu")}
|
||||||
</DropdownMenuTrigger>
|
</span>
|
||||||
<DropdownMenuContent align="end">
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<DropdownMenuItem
|
</Button>
|
||||||
onClick={() => {
|
</DropdownMenuTrigger>
|
||||||
setSelectedLabel(row.original);
|
<DropdownMenuContent align="end">
|
||||||
setIsEditModalOpen(true);
|
<DropdownMenuItem
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
setSelectedLabel(row.original);
|
||||||
{t("edit")}
|
setIsDeleteModalOpen(true);
|
||||||
</DropdownMenuItem>
|
}}
|
||||||
<DropdownMenuItem
|
>
|
||||||
onClick={() => {
|
<span className="text-red-500">
|
||||||
setSelectedLabel(row.original);
|
{t("delete")}
|
||||||
setIsDeleteModalOpen(true);
|
</span>
|
||||||
}}
|
</DropdownMenuItem>
|
||||||
>
|
</DropdownMenuContent>
|
||||||
<span className="text-red-500">
|
</DropdownMenu>
|
||||||
{t("delete")}
|
<Button
|
||||||
</span>
|
variant="outline"
|
||||||
</DropdownMenuItem>
|
onClick={() => {
|
||||||
</DropdownMenuContent>
|
setSelectedLabel(row.original);
|
||||||
</DropdownMenu>
|
setIsEditModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("edit")}
|
||||||
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import { SiKubernetes, SiNixos } from "react-icons/si";
|
import { SiKubernetes, SiNixos } from "react-icons/si";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|
||||||
export type CommandItem = string | { title: string; command: string };
|
export type CommandItem = string | { title: string; command: string };
|
||||||
|
|
||||||
@@ -50,9 +51,12 @@ export function NewtSiteInstallCommands({
|
|||||||
version = "latest"
|
version = "latest"
|
||||||
}: NewtSiteInstallCommandsProps) {
|
}: NewtSiteInstallCommandsProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const [acceptClients, setAcceptClients] = useState(true);
|
const [acceptClients, setAcceptClients] = useState(true);
|
||||||
const [allowPangolinSsh, setAllowPangolinSsh] = useState(true);
|
const [allowPangolinSsh, setAllowPangolinSsh] = useState(
|
||||||
|
!env.flags.disableEnterpriseFeatures
|
||||||
|
);
|
||||||
const [platform, setPlatform] = useState<Platform>("linux");
|
const [platform, setPlatform] = useState<Platform>("linux");
|
||||||
const [architecture, setArchitecture] = useState(
|
const [architecture, setArchitecture] = useState(
|
||||||
() => getArchitectures(platform)[0]
|
() => getArchitectures(platform)[0]
|
||||||
@@ -71,7 +75,11 @@ export function NewtSiteInstallCommands({
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const disableSshFlag =
|
const disableSshFlag =
|
||||||
supportsSshOption && !allowPangolinSsh ? " --disable-ssh" : "";
|
supportsSshOption &&
|
||||||
|
!allowPangolinSsh &&
|
||||||
|
!env.flags.disableEnterpriseFeatures
|
||||||
|
? " --disable-ssh"
|
||||||
|
: "";
|
||||||
const runAsRootPrefix =
|
const runAsRootPrefix =
|
||||||
supportsSshOption && allowPangolinSsh ? "sudo " : "";
|
supportsSshOption && allowPangolinSsh ? "sudo " : "";
|
||||||
|
|
||||||
@@ -306,27 +314,29 @@ WantedBy=default.target`
|
|||||||
>
|
>
|
||||||
{t("siteAcceptClientConnectionsDescription")}
|
{t("siteAcceptClientConnectionsDescription")}
|
||||||
</p>
|
</p>
|
||||||
{supportsSshOption && (
|
{supportsSshOption &&
|
||||||
<>
|
!env.flags.disableEnterpriseFeatures && (
|
||||||
<div className="flex items-center space-x-2 mb-2 mt-2">
|
<>
|
||||||
<CheckboxWithLabel
|
<div className="flex items-center space-x-2 mb-2 mt-2">
|
||||||
id="allowPangolinSsh"
|
<CheckboxWithLabel
|
||||||
checked={allowPangolinSsh}
|
id="allowPangolinSsh"
|
||||||
onCheckedChange={(checked) => {
|
checked={allowPangolinSsh}
|
||||||
const value = checked as boolean;
|
onCheckedChange={(checked) => {
|
||||||
setAllowPangolinSsh(value);
|
const value =
|
||||||
}}
|
checked as boolean;
|
||||||
label="Allow Pangolin SSH"
|
setAllowPangolinSsh(value);
|
||||||
/>
|
}}
|
||||||
</div>
|
label="Allow Pangolin SSH"
|
||||||
<p
|
/>
|
||||||
id="allowPangolinSsh-desc"
|
</div>
|
||||||
className="text-sm text-muted-foreground"
|
<p
|
||||||
>
|
id="allowPangolinSsh-desc"
|
||||||
{t("sitePangolinSshDescription")}
|
className="text-sm text-muted-foreground"
|
||||||
</p>
|
>
|
||||||
</>
|
{t("sitePangolinSshDescription")}
|
||||||
)}
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ export function EditPolicyForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const policyTiers = tierMatrix[TierFeature.ResourcePolicies];
|
const policyTiers = tierMatrix[TierFeature.ResourcePolicies];
|
||||||
const isDisabled = !isPaidUser(policyTiers);
|
const isInlinePolicy = hidePolicyNameForm && resourceId === undefined;
|
||||||
|
const showPaidAlert = !isInlinePolicy;
|
||||||
|
const isDisabled = showPaidAlert && !isPaidUser(policyTiers);
|
||||||
const effectiveReadonly = readonly || isDisabled;
|
const effectiveReadonly = readonly || isDisabled;
|
||||||
|
|
||||||
const authSection = (
|
const authSection = (
|
||||||
@@ -100,7 +102,7 @@ export function EditPolicyForm({
|
|||||||
if (section === "general") {
|
if (section === "general") {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaidFeaturesAlert tiers={policyTiers} />
|
{showPaidAlert && <PaidFeaturesAlert tiers={policyTiers} />}
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isDisabled
|
isDisabled
|
||||||
@@ -117,7 +119,7 @@ export function EditPolicyForm({
|
|||||||
if (section === "authentication") {
|
if (section === "authentication") {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaidFeaturesAlert tiers={policyTiers} />
|
{showPaidAlert && <PaidFeaturesAlert tiers={policyTiers} />}
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isDisabled
|
isDisabled
|
||||||
@@ -134,7 +136,7 @@ export function EditPolicyForm({
|
|||||||
if (section === "rules") {
|
if (section === "rules") {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaidFeaturesAlert tiers={policyTiers} />
|
{showPaidAlert && <PaidFeaturesAlert tiers={policyTiers} />}
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isDisabled
|
isDisabled
|
||||||
@@ -150,7 +152,7 @@ export function EditPolicyForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaidFeaturesAlert tiers={policyTiers} />
|
{showPaidAlert && <PaidFeaturesAlert tiers={policyTiers} />}
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isDisabled ? "pointer-events-none opacity-50" : undefined
|
isDisabled ? "pointer-events-none opacity-50" : undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user