mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-06 12:19:50 +00:00
Merge pull request #3240 from fosrl/dev
Fix small bugs with paid features, ui, docs
This commit is contained in:
@@ -214,6 +214,7 @@
|
|||||||
"resourceErrorDelte": "Error deleting resource",
|
"resourceErrorDelte": "Error deleting resource",
|
||||||
"resourcePoliciesBannerTitle": "Re-use Authentication and Access Rules",
|
"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.",
|
"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",
|
"resourcePoliciesTitle": "Manage Public Resource Policies",
|
||||||
"resourcePoliciesAttachedResourcesColumnTitle": "Resources",
|
"resourcePoliciesAttachedResourcesColumnTitle": "Resources",
|
||||||
"resourcePoliciesAttachedResources": "{count} resource(s)",
|
"resourcePoliciesAttachedResources": "{count} resource(s)",
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("newtAutoUpdateDescription")}{" "}
|
{t("newtAutoUpdateDescription")}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.pangolin.net/manage/sites/configure-site#auto-update"
|
href="https://docs.pangolin.net/manage/sites/auto-update"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ function RdpServerForm({
|
|||||||
sitesField="selectedSites"
|
sitesField="selectedSites"
|
||||||
destinationField="destination"
|
destinationField="destination"
|
||||||
destinationPortField="destinationPort"
|
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}
|
defaultPort={3389}
|
||||||
/>
|
/>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ function SshServerForm({
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("sshDaemonDisclaimer")}{" "}
|
{t("sshDaemonDisclaimer")}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.pangolin.net/manage/resources/public/ssh"
|
href="https://docs.pangolin.net/manage/ssh"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
@@ -589,7 +589,7 @@ function SshServerForm({
|
|||||||
sitesField="selectedSites"
|
sitesField="selectedSites"
|
||||||
destinationField="destination"
|
destinationField="destination"
|
||||||
destinationPortField="destinationPort"
|
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}
|
defaultPort={22}
|
||||||
/>
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
@@ -602,7 +602,7 @@ function SshServerForm({
|
|||||||
siteField="selectedSite"
|
siteField="selectedSite"
|
||||||
destinationField="destination"
|
destinationField="destination"
|
||||||
destinationPortField="destinationPort"
|
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}
|
defaultPort={22}
|
||||||
/>
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ import { toASCII } from "punycode";
|
|||||||
import {
|
import {
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
useTransition,
|
|
||||||
useEffect
|
useEffect
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useForm, type Resolver } from "react-hook-form";
|
import { useForm, type Resolver } from "react-hook-form";
|
||||||
@@ -229,7 +228,7 @@ export default function Page() {
|
|||||||
>([]);
|
>([]);
|
||||||
const [loadingExitNodes, setLoadingExitNodes] = useState(build === "saas");
|
const [loadingExitNodes, setLoadingExitNodes] = useState(build === "saas");
|
||||||
|
|
||||||
const [createLoading, startTransition] = useTransition();
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [showSnippets, setShowSnippets] = useState(false);
|
const [showSnippets, setShowSnippets] = useState(false);
|
||||||
const [niceId, setNiceId] = useState<string>("");
|
const [niceId, setNiceId] = useState<string>("");
|
||||||
|
|
||||||
@@ -461,6 +460,7 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
|
setCreateLoading(true);
|
||||||
const baseData = baseForm.getValues();
|
const baseData = baseForm.getValues();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -707,6 +707,8 @@ export default function Page() {
|
|||||||
t("resourceErrorCreateMessageDescription")
|
t("resourceErrorCreateMessageDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setCreateLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,7 +1099,7 @@ export default function Page() {
|
|||||||
"sshDaemonDisclaimer"
|
"sshDaemonDisclaimer"
|
||||||
)}{" "}
|
)}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.pangolin.net/manage/resources/public/ssh"
|
href="https://docs.pangolin.net/manage/ssh"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
@@ -1235,7 +1237,7 @@ export default function Page() {
|
|||||||
sitesField="selectedSites"
|
sitesField="selectedSites"
|
||||||
destinationField="destination"
|
destinationField="destination"
|
||||||
destinationPortField="destinationPort"
|
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}
|
defaultPort={22}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1256,7 +1258,7 @@ export default function Page() {
|
|||||||
siteField="selectedSite"
|
siteField="selectedSite"
|
||||||
destinationField="destination"
|
destinationField="destination"
|
||||||
destinationPortField="destinationPort"
|
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}
|
defaultPort={22}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1306,7 +1308,7 @@ export default function Page() {
|
|||||||
sitesField="selectedSites"
|
sitesField="selectedSites"
|
||||||
destinationField="destination"
|
destinationField="destination"
|
||||||
destinationPortField="destinationPort"
|
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}
|
defaultPort={3389}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1353,7 +1355,7 @@ export default function Page() {
|
|||||||
sitesField="selectedSites"
|
sitesField="selectedSites"
|
||||||
destinationField="destination"
|
destinationField="destination"
|
||||||
destinationPortField="destinationPort"
|
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}
|
defaultPort={5900}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1427,7 +1429,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={createLoading}
|
loading={createLoading}
|
||||||
disabled={!areAllTargetsValid() || browserGatewayDisabled}
|
disabled={!areAllTargetsValid() || browserGatewayDisabled || createLoading}
|
||||||
>
|
>
|
||||||
{t("resourceCreate")}
|
{t("resourceCreate")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ export default function GeneralPage() {
|
|||||||
"siteAutoUpdateDescription"
|
"siteAutoUpdateDescription"
|
||||||
)}{" "}
|
)}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.pangolin.net/manage/sites/configure-site#auto-update"
|
href="https://docs.pangolin.net/manage/sites/auto-update"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
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">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("sshPrivateKeyDisclaimer")}{" "}
|
{t("sshPrivateKeyDisclaimer")}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.pangolin.net/"
|
href="https://docs.pangolin.net/manage/ssh#authentication-method"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
|||||||
@@ -124,21 +124,21 @@ export default function VncClient({
|
|||||||
authToken: target.authToken
|
authToken: target.authToken
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const checkParams = new URLSearchParams(params);
|
// const checkParams = new URLSearchParams(params);
|
||||||
checkParams.set("checkOnly", "1");
|
// checkParams.set("checkOnly", "1");
|
||||||
const response = await fetch(`${base}?${checkParams.toString()}`);
|
// const response = await fetch(`${base}?${checkParams.toString()}`);
|
||||||
if (!response.ok) {
|
// if (!response.ok) {
|
||||||
const detail = (await response.text()).trim();
|
// const detail = (await response.text()).trim();
|
||||||
setConnectError(detail || t("sshErrorConnectionClosed"));
|
// setConnectError(detail || t("sshErrorConnectionClosed"));
|
||||||
setConnecting(false);
|
// setConnecting(false);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
} catch {
|
// } catch {
|
||||||
setConnectError(t("sshErrorWebSocket"));
|
// setConnectError(t("sshErrorWebSocket"));
|
||||||
setConnecting(false);
|
// setConnecting(false);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
let RFB: new (
|
let RFB: new (
|
||||||
target: HTMLElement,
|
target: HTMLElement,
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ export function BrowserGatewayTargetForm<T extends FieldValues>(
|
|||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
props.learnMoreHref ??
|
props.learnMoreHref ??
|
||||||
"https://docs.pangolin.net/manage/resources/public/ssh"
|
"https://docs.pangolin.net/manage/resources/private/multi-site-routing"
|
||||||
}
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
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 { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -18,6 +20,7 @@ import {
|
|||||||
CredenzaTitle
|
CredenzaTitle
|
||||||
} from "./Credenza";
|
} from "./Credenza";
|
||||||
import { OrgLabelForm } from "./OrgLabelForm";
|
import { OrgLabelForm } from "./OrgLabelForm";
|
||||||
|
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
export type CreateOrgLabelDialogProps = {
|
export type CreateOrgLabelDialogProps = {
|
||||||
@@ -35,6 +38,8 @@ export function CreateOrgLabelDialog({
|
|||||||
}: CreateOrgLabelDialogProps) {
|
}: CreateOrgLabelDialogProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const canManageLabels = isPaidUser(tierMatrix.labels);
|
||||||
const [isSubmitting, startTransition] = useTransition();
|
const [isSubmitting, startTransition] = useTransition();
|
||||||
|
|
||||||
async function createOrgLabel(data: { name: string; color: string }) {
|
async function createOrgLabel(data: { name: string; color: string }) {
|
||||||
@@ -79,8 +84,11 @@ export function CreateOrgLabelDialog({
|
|||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
|
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
||||||
<OrgLabelForm
|
<OrgLabelForm
|
||||||
|
disabled={!canManageLabels}
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
|
if (!canManageLabels) return;
|
||||||
startTransition(async () => createOrgLabel(data));
|
startTransition(async () => createOrgLabel(data));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -98,7 +106,7 @@ export function CreateOrgLabelDialog({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="org-label-form"
|
form="org-label-form"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || !canManageLabels}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{t("labelCreate")}
|
{t("labelCreate")}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
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 { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -18,6 +20,7 @@ import {
|
|||||||
CredenzaTitle
|
CredenzaTitle
|
||||||
} from "./Credenza";
|
} from "./Credenza";
|
||||||
import { OrgLabelForm } from "./OrgLabelForm";
|
import { OrgLabelForm } from "./OrgLabelForm";
|
||||||
|
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
export type EditOrgLabelDialogProps = {
|
export type EditOrgLabelDialogProps = {
|
||||||
@@ -41,6 +44,8 @@ export function EditOrgLabelDialog({
|
|||||||
}: EditOrgLabelDialogProps) {
|
}: EditOrgLabelDialogProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const canManageLabels = isPaidUser(tierMatrix.labels);
|
||||||
const [isSubmitting, startTransition] = useTransition();
|
const [isSubmitting, startTransition] = useTransition();
|
||||||
|
|
||||||
async function editOrgLabel(data: { name: string; color: string }) {
|
async function editOrgLabel(data: { name: string; color: string }) {
|
||||||
@@ -85,9 +90,12 @@ export function EditOrgLabelDialog({
|
|||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
|
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
||||||
<OrgLabelForm
|
<OrgLabelForm
|
||||||
|
disabled={!canManageLabels}
|
||||||
defaultValue={label}
|
defaultValue={label}
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
|
if (!canManageLabels) return;
|
||||||
startTransition(async () => editOrgLabel(data));
|
startTransition(async () => editOrgLabel(data));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -105,7 +113,7 @@ export function EditOrgLabelDialog({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="org-label-form"
|
form="org-label-form"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || !canManageLabels}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{t("labelEdit")}
|
{t("labelEdit")}
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ export type LabelFormData = z.infer<typeof labelFormSchema>;
|
|||||||
export type OrgLabelFormProps = {
|
export type OrgLabelFormProps = {
|
||||||
onSubmit: (data: LabelFormData) => void;
|
onSubmit: (data: LabelFormData) => void;
|
||||||
defaultValue?: LabelFormData;
|
defaultValue?: LabelFormData;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
|
export function OrgLabelForm({
|
||||||
|
onSubmit,
|
||||||
|
defaultValue,
|
||||||
|
disabled = false
|
||||||
|
}: OrgLabelFormProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const colorValues = Object.values(LABEL_COLORS);
|
const colorValues = Object.values(LABEL_COLORS);
|
||||||
@@ -70,9 +75,7 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("labelNameField")}</FormLabel>
|
<FormLabel>{t("labelNameField")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} disabled={disabled} />
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -88,6 +91,7 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
|
|||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
@@ -110,7 +114,9 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span data-name>
|
<span data-name>
|
||||||
{color.charAt(0).toUpperCase() +
|
{color
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase() +
|
||||||
color.slice(1)}
|
color.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -2079,7 +2079,7 @@ export function PrivateResourceForm({
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("sshDaemonDisclaimer")}{" "}
|
{t("sshDaemonDisclaimer")}{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.pangolin.net/manage/resources/private/ssh"
|
href="https://docs.pangolin.net/manage/ssh"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
|||||||
@@ -767,7 +767,7 @@ function TargetStatusCell({
|
|||||||
|
|
||||||
if (!targets || targets.length === 0) {
|
if (!targets || targets.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 px-2">
|
<div className="flex items-center gap-2 px-0">
|
||||||
<StatusIcon status="unknown" />
|
<StatusIcon status="unknown" />
|
||||||
<span className="text-sm">{t("resourcesTableNoTargets")}</span>
|
<span className="text-sm">{t("resourcesTableNoTargets")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"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 { useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
import DismissableBanner from "./DismissableBanner";
|
import DismissableBanner from "./DismissableBanner";
|
||||||
|
|
||||||
export const ResourcePoliciesBanner = () => {
|
export const ResourcePoliciesBanner = () => {
|
||||||
@@ -14,7 +16,22 @@ export const ResourcePoliciesBanner = () => {
|
|||||||
title={t("resourcePoliciesBannerTitle")}
|
title={t("resourcePoliciesBannerTitle")}
|
||||||
titleIcon={<Shield className="w-5 h-5 text-primary" />}
|
titleIcon={<Shield className="w-5 h-5 text-primary" />}
|
||||||
description={t("resourcePoliciesBannerDescription")}
|
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", {
|
{t.rich("siteInstallAdvantechDocsDescription", {
|
||||||
docsLink: (chunks) => (
|
docsLink: (chunks) => (
|
||||||
<a
|
<a
|
||||||
href="https://docs.pangolin.net/manage/sites/install-advantech"
|
href="https://docs.pangolin.net/manage/sites/install-site"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
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 { orgQueries } from "@app/lib/queries";
|
||||||
import { build } from "@server/build";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { EditPolicyNameSectionForm } from "./EditPolicyNameSectionForm";
|
import { EditPolicyNameSectionForm } from "./EditPolicyNameSectionForm";
|
||||||
import { PolicyAuthStackSection } from "./PolicyAuthStackSection";
|
import { PolicyAuthStackSection } from "./PolicyAuthStackSection";
|
||||||
import { PolicyAccessRulesSection } from "./PolicyAccessRulesSection";
|
import { PolicyAccessRulesSection } from "./PolicyAccessRulesSection";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
|
|
||||||
export type EditPolicyFormSection = "general" | "authentication" | "rules";
|
export type EditPolicyFormSection = "general" | "authentication" | "rules";
|
||||||
|
|
||||||
@@ -71,13 +72,17 @@ export function EditPolicyForm({
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const policyTiers = tierMatrix[TierFeature.ResourcePolicies];
|
||||||
|
const isDisabled = !isPaidUser(policyTiers);
|
||||||
|
const effectiveReadonly = readonly || isDisabled;
|
||||||
|
|
||||||
const authSection = (
|
const authSection = (
|
||||||
<PolicyAuthStackSection
|
<PolicyAuthStackSection
|
||||||
mode="edit"
|
mode="edit"
|
||||||
orgId={org.org.orgId}
|
orgId={org.org.orgId}
|
||||||
allIdps={allIdps}
|
allIdps={allIdps}
|
||||||
emailEnabled={env.email.emailEnabled}
|
emailEnabled={env.email.emailEnabled}
|
||||||
readonly={readonly}
|
readonly={effectiveReadonly}
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -87,32 +92,82 @@ export function EditPolicyForm({
|
|||||||
mode="edit"
|
mode="edit"
|
||||||
isMaxmindAvailable={isMaxmindAvailable}
|
isMaxmindAvailable={isMaxmindAvailable}
|
||||||
isMaxmindAsnAvailable={isMaxmindASNAvailable}
|
isMaxmindAsnAvailable={isMaxmindASNAvailable}
|
||||||
readonly={readonly}
|
readonly={effectiveReadonly}
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (section === "general") {
|
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") {
|
if (section === "authentication") {
|
||||||
return authSection;
|
return (
|
||||||
|
<>
|
||||||
|
<PaidFeaturesAlert tiers={policyTiers} />
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDisabled
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{authSection}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section === "rules") {
|
if (section === "rules") {
|
||||||
return rulesSection;
|
return (
|
||||||
|
<>
|
||||||
|
<PaidFeaturesAlert tiers={policyTiers} />
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDisabled
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{rulesSection}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<>
|
||||||
{!hidePolicyNameForm && !isOverlay && (
|
<PaidFeaturesAlert tiers={policyTiers} />
|
||||||
<EditPolicyNameSectionForm readonly={readonly} />
|
<div
|
||||||
)}
|
className={
|
||||||
|
isDisabled ? "pointer-events-none opacity-50" : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsContainer>
|
||||||
|
{!hidePolicyNameForm && !isOverlay && (
|
||||||
|
<EditPolicyNameSectionForm
|
||||||
|
readonly={effectiveReadonly}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{authSection}
|
{authSection}
|
||||||
|
|
||||||
{rulesSection}
|
{rulesSection}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user