mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-11 18:09:05 +00:00
Compare commits
10 Commits
dependabot
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71273e1b1c | ||
|
|
02f6e2a8c3 | ||
|
|
1d9c4dd9e2 | ||
|
|
b9dd0c8e43 | ||
|
|
cd052976eb | ||
|
|
cc498f0e33 | ||
|
|
1a942937e6 | ||
|
|
d81d1a6b7f | ||
|
|
f64d04e827 | ||
|
|
540aee3fe2 |
@@ -214,6 +214,7 @@
|
||||
"resourceErrorDelte": "Error deleting resource",
|
||||
"resourcePoliciesBannerTitle": "Re-use Authentication and Access Rules",
|
||||
"resourcePoliciesBannerDescription": "Shared resource policies let you define authentication methods and access rules once, then attach them to multiple public resources. When you update a policy, every linked resource inherits the change automatically.",
|
||||
"resourcePoliciesBannerButtonText": "Learn More",
|
||||
"resourcePoliciesTitle": "Manage Public Resource Policies",
|
||||
"resourcePoliciesAttachedResourcesColumnTitle": "Resources",
|
||||
"resourcePoliciesAttachedResources": "{count} resource(s)",
|
||||
|
||||
1634
package-lock.json
generated
1634
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -35,7 +35,7 @@
|
||||
"@asteasolutions/zod-to-openapi": "8.5.0",
|
||||
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
|
||||
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz",
|
||||
"@aws-sdk/client-s3": "3.1066.0",
|
||||
"@aws-sdk/client-s3": "3.1056.0",
|
||||
"@headlessui/react": "2.2.10",
|
||||
"@hookform/resolvers": "5.4.0",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
@@ -52,12 +52,12 @@
|
||||
"@radix-ui/react-label": "2.1.8",
|
||||
"@radix-ui/react-popover": "1.1.15",
|
||||
"@radix-ui/react-progress": "1.1.8",
|
||||
"@radix-ui/react-radio-group": "1.4.0",
|
||||
"@radix-ui/react-radio-group": "1.3.8",
|
||||
"@radix-ui/react-scroll-area": "1.2.10",
|
||||
"@radix-ui/react-select": "2.3.0",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.8",
|
||||
"@radix-ui/react-slot": "1.2.4",
|
||||
"@radix-ui/react-switch": "1.3.0",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
@@ -68,13 +68,13 @@
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@simplewebauthn/server": "13.3.1",
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
"@tanstack/react-query": "5.101.0",
|
||||
"@tanstack/react-query": "5.100.14",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"arctic": "3.7.0",
|
||||
"axios": "1.17.0",
|
||||
"axios": "1.16.1",
|
||||
"better-sqlite3": "11.9.1",
|
||||
"canvas-confetti": "1.9.4",
|
||||
"class-variance-authority": "0.7.1",
|
||||
@@ -93,7 +93,7 @@
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.11.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.2.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "1.17.0",
|
||||
"maxmind": "5.0.6",
|
||||
@@ -106,13 +106,13 @@
|
||||
"nodemailer": "8.0.9",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.21.0",
|
||||
"posthog-node": "5.36.15",
|
||||
"posthog-node": "5.35.6",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.6",
|
||||
"react-day-picker": "9.14.0",
|
||||
"react-dom": "19.2.6",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.78.0",
|
||||
"react-hook-form": "7.76.1",
|
||||
"react-icons": "5.6.0",
|
||||
"recharts": "3.8.1",
|
||||
"reodotdev": "1.1.0",
|
||||
|
||||
@@ -327,27 +327,6 @@ export async function listSites(
|
||||
);
|
||||
}
|
||||
|
||||
let accessibleSites;
|
||||
if (req.user) {
|
||||
accessibleSites = await db
|
||||
.select({
|
||||
siteId: sql<number>`COALESCE(${userSites.siteId}, ${roleSites.siteId})`
|
||||
})
|
||||
.from(userSites)
|
||||
.fullJoin(roleSites, eq(userSites.siteId, roleSites.siteId))
|
||||
.where(
|
||||
or(
|
||||
eq(userSites.userId, req.user!.userId),
|
||||
inArray(roleSites.roleId, req.userOrgRoleIds!)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
accessibleSites = await db
|
||||
.select({ siteId: sites.siteId })
|
||||
.from(sites)
|
||||
.where(eq(sites.orgId, orgId));
|
||||
}
|
||||
|
||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
tierMatrix.labels
|
||||
@@ -364,14 +343,38 @@ export async function listSites(
|
||||
labels: labelFilter
|
||||
} = parsedQuery.data;
|
||||
|
||||
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
|
||||
const conditions = [eq(sites.orgId, orgId)];
|
||||
|
||||
const conditions = [
|
||||
and(
|
||||
inArray(sites.siteId, accessibleSiteIds),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
];
|
||||
if (req.user) {
|
||||
const userAccessConditions = [
|
||||
inArray(
|
||||
sites.siteId,
|
||||
db
|
||||
.select({ siteId: userSites.siteId })
|
||||
.from(userSites)
|
||||
.where(eq(userSites.userId, req.user.userId))
|
||||
)
|
||||
];
|
||||
|
||||
const roleIds = req.userOrgRoleIds ?? [];
|
||||
if (roleIds.length > 0) {
|
||||
userAccessConditions.push(
|
||||
inArray(
|
||||
sites.siteId,
|
||||
db
|
||||
.select({ siteId: roleSites.siteId })
|
||||
.from(roleSites)
|
||||
.where(inArray(roleSites.roleId, roleIds))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
conditions.push(
|
||||
userAccessConditions.length === 1
|
||||
? userAccessConditions[0]
|
||||
: or(...userAccessConditions)!
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof online !== "undefined") {
|
||||
conditions.push(eq(sites.online, online));
|
||||
@@ -418,17 +421,15 @@ export async function listSites(
|
||||
)
|
||||
);
|
||||
}
|
||||
conditions.push(or(...queryList));
|
||||
conditions.push(or(...queryList)!);
|
||||
}
|
||||
|
||||
const baseQuery = querySitesBase().where(and(...conditions));
|
||||
|
||||
// we need to add `as` so that drizzle filters the result as a subquery
|
||||
const countQuery = db.$count(
|
||||
querySitesBase()
|
||||
.where(and(...conditions))
|
||||
.as("filtered_sites")
|
||||
);
|
||||
const countQuery = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(sites)
|
||||
.where(and(...conditions));
|
||||
|
||||
const siteListQuery = baseQuery
|
||||
.limit(pageSize)
|
||||
@@ -441,11 +442,13 @@ export async function listSites(
|
||||
: asc(sites.name)
|
||||
);
|
||||
|
||||
const [totalCount, rows] = await Promise.all([
|
||||
const [countRows, rows] = await Promise.all([
|
||||
countQuery,
|
||||
siteListQuery
|
||||
]);
|
||||
|
||||
const totalCount = Number(countRows[0]?.count ?? 0);
|
||||
|
||||
// Get latest version asynchronously without blocking the response
|
||||
const latestNewtVersionPromise = getLatestNewtVersion();
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
||||
<FormDescription>
|
||||
{t("newtAutoUpdateDescription")}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/sites/configure-site#auto-update"
|
||||
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"
|
||||
|
||||
@@ -229,7 +229,7 @@ function RdpServerForm({
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp#site-and-host-configuration"
|
||||
defaultPort={3389}
|
||||
/>
|
||||
</SettingsSectionForm>
|
||||
|
||||
@@ -467,7 +467,7 @@ function SshServerForm({
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("sshDaemonDisclaimer")}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
href="https://docs.pangolin.net/manage/ssh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
@@ -589,7 +589,7 @@ function SshServerForm({
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh#site-and-host-configuration"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
@@ -602,7 +602,7 @@ function SshServerForm({
|
||||
siteField="selectedSite"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh#site-and-host-configuration"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
@@ -80,7 +80,6 @@ import { toASCII } from "punycode";
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
useTransition,
|
||||
useEffect
|
||||
} from "react";
|
||||
import { useForm, type Resolver } from "react-hook-form";
|
||||
@@ -229,7 +228,7 @@ export default function Page() {
|
||||
>([]);
|
||||
const [loadingExitNodes, setLoadingExitNodes] = useState(build === "saas");
|
||||
|
||||
const [createLoading, startTransition] = useTransition();
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [showSnippets, setShowSnippets] = useState(false);
|
||||
const [niceId, setNiceId] = useState<string>("");
|
||||
|
||||
@@ -461,6 +460,7 @@ export default function Page() {
|
||||
};
|
||||
|
||||
async function onSubmit() {
|
||||
setCreateLoading(true);
|
||||
const baseData = baseForm.getValues();
|
||||
|
||||
try {
|
||||
@@ -707,6 +707,8 @@ export default function Page() {
|
||||
t("resourceErrorCreateMessageDescription")
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setCreateLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,7 +764,7 @@ export default function Page() {
|
||||
ssh: "SSH",
|
||||
rdp: "RDP",
|
||||
vnc: "VNC",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const typeOptions: OptionSelectOption<NewResourceType>[] =
|
||||
@@ -1097,7 +1099,7 @@ export default function Page() {
|
||||
"sshDaemonDisclaimer"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
href="https://docs.pangolin.net/manage/ssh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
@@ -1235,7 +1237,7 @@ export default function Page() {
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh#site-and-host-configuration"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</Form>
|
||||
@@ -1256,7 +1258,7 @@ export default function Page() {
|
||||
siteField="selectedSite"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/ssh#site-and-host-configuration"
|
||||
defaultPort={22}
|
||||
/>
|
||||
</Form>
|
||||
@@ -1306,7 +1308,7 @@ export default function Page() {
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/rdp#site-and-host-configuration"
|
||||
defaultPort={3389}
|
||||
/>
|
||||
</Form>
|
||||
@@ -1353,7 +1355,7 @@ export default function Page() {
|
||||
sitesField="selectedSites"
|
||||
destinationField="destination"
|
||||
destinationPortField="destinationPort"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/vnc"
|
||||
learnMoreHref="https://docs.pangolin.net/manage/resources/public/vnc#site-and-host-configuration"
|
||||
defaultPort={5900}
|
||||
/>
|
||||
</Form>
|
||||
@@ -1427,7 +1429,7 @@ export default function Page() {
|
||||
}
|
||||
}}
|
||||
loading={createLoading}
|
||||
disabled={!areAllTargetsValid() || browserGatewayDisabled}
|
||||
disabled={!areAllTargetsValid() || browserGatewayDisabled || createLoading}
|
||||
>
|
||||
{t("resourceCreate")}
|
||||
</Button>
|
||||
|
||||
@@ -317,7 +317,7 @@ export default function GeneralPage() {
|
||||
"siteAutoUpdateDescription"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/sites/configure-site#auto-update"
|
||||
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"
|
||||
|
||||
@@ -596,7 +596,7 @@ export default function SshClient({
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("sshPrivateKeyDisclaimer")}{" "}
|
||||
<Link
|
||||
href="https://docs.pangolin.net/"
|
||||
href="https://docs.pangolin.net/manage/ssh#authentication-method"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
|
||||
@@ -124,21 +124,21 @@ export default function VncClient({
|
||||
authToken: target.authToken
|
||||
});
|
||||
|
||||
try {
|
||||
const checkParams = new URLSearchParams(params);
|
||||
checkParams.set("checkOnly", "1");
|
||||
const response = await fetch(`${base}?${checkParams.toString()}`);
|
||||
if (!response.ok) {
|
||||
const detail = (await response.text()).trim();
|
||||
setConnectError(detail || t("sshErrorConnectionClosed"));
|
||||
setConnecting(false);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
setConnectError(t("sshErrorWebSocket"));
|
||||
setConnecting(false);
|
||||
return;
|
||||
}
|
||||
// try {
|
||||
// const checkParams = new URLSearchParams(params);
|
||||
// checkParams.set("checkOnly", "1");
|
||||
// const response = await fetch(`${base}?${checkParams.toString()}`);
|
||||
// if (!response.ok) {
|
||||
// const detail = (await response.text()).trim();
|
||||
// setConnectError(detail || t("sshErrorConnectionClosed"));
|
||||
// setConnecting(false);
|
||||
// return;
|
||||
// }
|
||||
// } catch {
|
||||
// setConnectError(t("sshErrorWebSocket"));
|
||||
// setConnecting(false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
let RFB: new (
|
||||
target: HTMLElement,
|
||||
|
||||
@@ -222,7 +222,7 @@ export function BrowserGatewayTargetForm<T extends FieldValues>(
|
||||
<a
|
||||
href={
|
||||
props.learnMoreHref ??
|
||||
"https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
"https://docs.pangolin.net/manage/resources/private/multi-site-routing"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
CredenzaTitle
|
||||
} from "./Credenza";
|
||||
import { OrgLabelForm } from "./OrgLabelForm";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export type CreateOrgLabelDialogProps = {
|
||||
@@ -35,6 +38,8 @@ export function CreateOrgLabelDialog({
|
||||
}: CreateOrgLabelDialogProps) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const canManageLabels = isPaidUser(tierMatrix.labels);
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
|
||||
async function createOrgLabel(data: { name: string; color: string }) {
|
||||
@@ -79,8 +84,11 @@ export function CreateOrgLabelDialog({
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
||||
<OrgLabelForm
|
||||
disabled={!canManageLabels}
|
||||
onSubmit={(data) => {
|
||||
if (!canManageLabels) return;
|
||||
startTransition(async () => createOrgLabel(data));
|
||||
}}
|
||||
/>
|
||||
@@ -98,7 +106,7 @@ export function CreateOrgLabelDialog({
|
||||
<Button
|
||||
type="submit"
|
||||
form="org-label-form"
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || !canManageLabels}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{t("labelCreate")}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
CredenzaTitle
|
||||
} from "./Credenza";
|
||||
import { OrgLabelForm } from "./OrgLabelForm";
|
||||
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export type EditOrgLabelDialogProps = {
|
||||
@@ -41,6 +44,8 @@ export function EditOrgLabelDialog({
|
||||
}: EditOrgLabelDialogProps) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
const canManageLabels = isPaidUser(tierMatrix.labels);
|
||||
const [isSubmitting, startTransition] = useTransition();
|
||||
|
||||
async function editOrgLabel(data: { name: string; color: string }) {
|
||||
@@ -85,9 +90,12 @@ export function EditOrgLabelDialog({
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
||||
<OrgLabelForm
|
||||
disabled={!canManageLabels}
|
||||
defaultValue={label}
|
||||
onSubmit={(data) => {
|
||||
if (!canManageLabels) return;
|
||||
startTransition(async () => editOrgLabel(data));
|
||||
}}
|
||||
/>
|
||||
@@ -105,7 +113,7 @@ export function EditOrgLabelDialog({
|
||||
<Button
|
||||
type="submit"
|
||||
form="org-label-form"
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || !canManageLabels}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{t("labelEdit")}
|
||||
|
||||
@@ -35,9 +35,14 @@ export type LabelFormData = z.infer<typeof labelFormSchema>;
|
||||
export type OrgLabelFormProps = {
|
||||
onSubmit: (data: LabelFormData) => void;
|
||||
defaultValue?: LabelFormData;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
|
||||
export function OrgLabelForm({
|
||||
onSubmit,
|
||||
defaultValue,
|
||||
disabled = false
|
||||
}: OrgLabelFormProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const colorValues = Object.values(LABEL_COLORS);
|
||||
@@ -70,9 +75,7 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
|
||||
<FormItem>
|
||||
<FormLabel>{t("labelNameField")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} disabled={disabled} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -88,6 +91,7 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
@@ -110,7 +114,9 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
|
||||
}}
|
||||
/>
|
||||
<span data-name>
|
||||
{color.charAt(0).toUpperCase() +
|
||||
{color
|
||||
.charAt(0)
|
||||
.toUpperCase() +
|
||||
color.slice(1)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
|
||||
@@ -2079,7 +2079,7 @@ export function PrivateResourceForm({
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("sshDaemonDisclaimer")}{" "}
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/resources/private/ssh"
|
||||
href="https://docs.pangolin.net/manage/ssh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
|
||||
@@ -767,7 +767,7 @@ function TargetStatusCell({
|
||||
|
||||
if (!targets || targets.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<div className="flex items-center gap-2 px-0">
|
||||
<StatusIcon status="unknown" />
|
||||
<span className="text-sm">{t("resourcesTableNoTargets")}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Shield } from "lucide-react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Shield, ArrowRight } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import DismissableBanner from "./DismissableBanner";
|
||||
|
||||
export const ResourcePoliciesBanner = () => {
|
||||
@@ -14,7 +16,22 @@ export const ResourcePoliciesBanner = () => {
|
||||
title={t("resourcePoliciesBannerTitle")}
|
||||
titleIcon={<Shield className="w-5 h-5 text-primary" />}
|
||||
description={t("resourcePoliciesBannerDescription")}
|
||||
/>
|
||||
>
|
||||
<Link
|
||||
href="https://docs.pangolin.net/manage/resources/public/resource-policies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||
>
|
||||
{t("resourcePoliciesBannerButtonText")}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</DismissableBanner>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -354,7 +354,7 @@ WantedBy=default.target`
|
||||
{t.rich("siteInstallAdvantechDocsDescription", {
|
||||
docsLink: (chunks) => (
|
||||
<a
|
||||
href="https://docs.pangolin.net/manage/sites/install-advantech"
|
||||
href="https://docs.pangolin.net/manage/sites/install-site"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
|
||||
@@ -8,13 +8,14 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
|
||||
import { orgQueries } from "@app/lib/queries";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { EditPolicyNameSectionForm } from "./EditPolicyNameSectionForm";
|
||||
import { PolicyAuthStackSection } from "./PolicyAuthStackSection";
|
||||
import { PolicyAccessRulesSection } from "./PolicyAccessRulesSection";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
|
||||
export type EditPolicyFormSection = "general" | "authentication" | "rules";
|
||||
|
||||
@@ -71,13 +72,17 @@ export function EditPolicyForm({
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const policyTiers = tierMatrix[TierFeature.ResourcePolicies];
|
||||
const isDisabled = !isPaidUser(policyTiers);
|
||||
const effectiveReadonly = readonly || isDisabled;
|
||||
|
||||
const authSection = (
|
||||
<PolicyAuthStackSection
|
||||
mode="edit"
|
||||
orgId={org.org.orgId}
|
||||
allIdps={allIdps}
|
||||
emailEnabled={env.email.emailEnabled}
|
||||
readonly={readonly}
|
||||
readonly={effectiveReadonly}
|
||||
resourceId={resourceId}
|
||||
/>
|
||||
);
|
||||
@@ -87,32 +92,82 @@ export function EditPolicyForm({
|
||||
mode="edit"
|
||||
isMaxmindAvailable={isMaxmindAvailable}
|
||||
isMaxmindAsnAvailable={isMaxmindASNAvailable}
|
||||
readonly={readonly}
|
||||
readonly={effectiveReadonly}
|
||||
resourceId={resourceId}
|
||||
/>
|
||||
);
|
||||
|
||||
if (section === "general") {
|
||||
return <EditPolicyNameSectionForm readonly={readonly} />;
|
||||
return (
|
||||
<>
|
||||
<PaidFeaturesAlert tiers={policyTiers} />
|
||||
<div
|
||||
className={
|
||||
isDisabled
|
||||
? "pointer-events-none opacity-50"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EditPolicyNameSectionForm readonly={effectiveReadonly} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (section === "authentication") {
|
||||
return authSection;
|
||||
return (
|
||||
<>
|
||||
<PaidFeaturesAlert tiers={policyTiers} />
|
||||
<div
|
||||
className={
|
||||
isDisabled
|
||||
? "pointer-events-none opacity-50"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{authSection}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (section === "rules") {
|
||||
return rulesSection;
|
||||
return (
|
||||
<>
|
||||
<PaidFeaturesAlert tiers={policyTiers} />
|
||||
<div
|
||||
className={
|
||||
isDisabled
|
||||
? "pointer-events-none opacity-50"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{rulesSection}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{!hidePolicyNameForm && !isOverlay && (
|
||||
<EditPolicyNameSectionForm readonly={readonly} />
|
||||
)}
|
||||
<>
|
||||
<PaidFeaturesAlert tiers={policyTiers} />
|
||||
<div
|
||||
className={
|
||||
isDisabled ? "pointer-events-none opacity-50" : undefined
|
||||
}
|
||||
>
|
||||
<SettingsContainer>
|
||||
{!hidePolicyNameForm && !isOverlay && (
|
||||
<EditPolicyNameSectionForm
|
||||
readonly={effectiveReadonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{authSection}
|
||||
{authSection}
|
||||
|
||||
{rulesSection}
|
||||
</SettingsContainer>
|
||||
{rulesSection}
|
||||
</SettingsContainer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user