add learn more link

This commit is contained in:
miloschwartz
2026-03-29 11:34:07 -07:00
parent d1b2105c80
commit bff2ba7cc2
7 changed files with 420 additions and 383 deletions

View File

@@ -45,6 +45,7 @@ import { useTranslations } from "next-intl";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget"; import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { import {
@@ -493,7 +494,7 @@ export default function GeneralPage() {
{t("idpAutoProvisionUsers")} {t("idpAutoProvisionUsers")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("idpAutoProvisionUsersDescription")} <IdpAutoProvisionUsersDescription />
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget"; import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { import {
SettingsContainer, SettingsContainer,
@@ -296,7 +297,7 @@ export default function Page() {
{t("idpAutoProvisionUsers")} {t("idpAutoProvisionUsers")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("idpAutoProvisionUsersDescription")} <IdpAutoProvisionUsersDescription />
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>

View File

@@ -31,6 +31,7 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { import {
InfoSection, InfoSection,
@@ -349,7 +350,7 @@ export default function GeneralPage() {
{t("idpAutoProvisionUsers")} {t("idpAutoProvisionUsers")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("idpAutoProvisionUsersDescription")} <IdpAutoProvisionUsersDescription />
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@@ -375,9 +376,6 @@ export default function GeneralPage() {
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-sm text-muted-foreground">
{t("idpAutoProvisionUsersDescription")}
</span>
{form.watch("autoProvision") && ( {form.watch("autoProvision") && (
<FormDescription> <FormDescription>
{t.rich( {t.rich(

View File

@@ -22,6 +22,7 @@ import {
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import HeaderTitle from "@app/components/SettingsSectionTitle"; import HeaderTitle from "@app/components/SettingsSectionTitle";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
@@ -94,8 +95,7 @@ export default function Page() {
const watchedType = form.watch("type"); const watchedType = form.watch("type");
const templatesLocked = const templatesLocked =
!templatesPaid && !templatesPaid && (watchedType === "google" || watchedType === "azure");
(watchedType === "google" || watchedType === "azure");
async function onSubmit(data: CreateIdpFormValues) { async function onSubmit(data: CreateIdpFormValues) {
if ( if (
@@ -223,7 +223,9 @@ export default function Page() {
<div className="flex items-start mb-0"> <div className="flex items-start mb-0">
<SwitchInput <SwitchInput
id="auto-provision-toggle" id="auto-provision-toggle"
label={t("idpAutoProvisionUsers")} label={t(
"idpAutoProvisionUsers"
)}
defaultChecked={form.getValues( defaultChecked={form.getValues(
"autoProvision" "autoProvision"
)} )}
@@ -235,11 +237,6 @@ export default function Page() {
}} }}
/> />
</div> </div>
<span className="text-sm text-muted-foreground">
{t(
"idpAutoProvisionUsersDescription"
)}
</span>
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
@@ -251,391 +248,409 @@ export default function Page() {
disabled={templatesLocked} disabled={templatesLocked}
className="min-w-0 border-0 p-0 m-0 disabled:pointer-events-none disabled:opacity-60" className="min-w-0 border-0 p-0 m-0 disabled:pointer-events-none disabled:opacity-60"
> >
{watchedType === "google" && ( {watchedType === "google" && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("idpGoogleConfigurationTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("idpGoogleConfigurationDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...form}>
<form
className="space-y-4"
id="create-idp-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpClientId")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpGoogleClientIdDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpClientSecret")}
</FormLabel>
<FormControl>
<Input
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t(
"idpGoogleClientSecretDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
{watchedType === "azure" && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("idpAzureConfigurationTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("idpAzureConfigurationDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...form}>
<form
className="space-y-4"
id="create-idp-form"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="tenantId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpTenantIdLabel")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpAzureTenantIdDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpClientId")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpAzureClientIdDescription2"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpClientSecret")}
</FormLabel>
<FormControl>
<Input
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t(
"idpAzureClientSecretDescription2"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
{watchedType === "oidc" && (
<SettingsSectionGrid cols={2}>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("idpOidcConfigure")} {t("idpGoogleConfigurationTitle")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("idpOidcConfigureDescription")} {t("idpGoogleConfigurationDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<Form {...form}> <SettingsSectionForm>
<form <Form {...form}>
className="space-y-4" <form
id="create-idp-form" className="space-y-4"
onSubmit={form.handleSubmit(onSubmit)} id="create-idp-form"
> onSubmit={form.handleSubmit(
<FormField onSubmit
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpClientId")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpClientIdDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)} )}
/> >
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpClientId")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpGoogleClientIdDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="clientSecret" name="clientSecret"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t("idpClientSecret")} {t(
</FormLabel> "idpClientSecret"
<FormControl> )}
<Input </FormLabel>
type="password" <FormControl>
{...field} <Input
/> type="password"
</FormControl> {...field}
<FormDescription> />
{t( </FormControl>
"idpClientSecretDescription" <FormDescription>
)} {t(
</FormDescription> "idpGoogleClientSecretDescription"
<FormMessage /> )}
</FormItem> </FormDescription>
)} <FormMessage />
/> </FormItem>
)}
<FormField />
control={form.control} </form>
name="authUrl" </Form>
render={({ field }) => ( </SettingsSectionForm>
<FormItem>
<FormLabel>
{t("idpAuthUrl")}
</FormLabel>
<FormControl>
<Input
placeholder="https://your-idp.com/oauth2/authorize"
{...field}
/>
</FormControl>
<FormDescription>
{t(
"idpAuthUrlDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tokenUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpTokenUrl")}
</FormLabel>
<FormControl>
<Input
placeholder="https://your-idp.com/oauth2/token"
{...field}
/>
</FormControl>
<FormDescription>
{t(
"idpTokenUrlDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
)}
{watchedType === "azure" && (
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
{t("idpToken")} {t("idpAzureConfigurationTitle")}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("idpTokenDescription")} {t("idpAzureConfigurationDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<Form {...form}> <SettingsSectionForm>
<form <Form {...form}>
className="space-y-4" <form
id="create-idp-form" className="space-y-4"
onSubmit={form.handleSubmit(onSubmit)} id="create-idp-form"
> onSubmit={form.handleSubmit(
<FormField onSubmit
control={form.control}
name="identifierPath"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpJmespathLabel")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpJmespathLabelDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)} )}
/> >
<FormField
control={form.control}
name="tenantId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"idpTenantIdLabel"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpAzureTenantIdDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="emailPath" name="clientId"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t( {t("idpClientId")}
"idpJmespathEmailPathOptional" </FormLabel>
)} <FormControl>
</FormLabel> <Input {...field} />
<FormControl> </FormControl>
<Input {...field} /> <FormDescription>
</FormControl> {t(
<FormDescription> "idpAzureClientIdDescription2"
{t( )}
"idpJmespathEmailPathOptionalDescription" </FormDescription>
)} <FormMessage />
</FormDescription> </FormItem>
<FormMessage /> )}
</FormItem> />
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="namePath" name="clientSecret"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t( {t(
"idpJmespathNamePathOptional" "idpClientSecret"
)} )}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input
</FormControl> type="password"
<FormDescription> {...field}
{t( />
"idpJmespathNamePathOptionalDescription" </FormControl>
)} <FormDescription>
</FormDescription> {t(
<FormMessage /> "idpAzureClientSecretDescription2"
</FormItem> )}
)} </FormDescription>
/> <FormMessage />
</FormItem>
<FormField )}
control={form.control} />
name="scopes" </form>
render={({ field }) => ( </Form>
<FormItem> </SettingsSectionForm>
<FormLabel>
{t(
"idpOidcConfigureScopes"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpOidcConfigureScopesDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
</SettingsSectionGrid> )}
)}
{watchedType === "oidc" && (
<SettingsSectionGrid cols={2}>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("idpOidcConfigure")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("idpOidcConfigureDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<Form {...form}>
<form
className="space-y-4"
id="create-idp-form"
onSubmit={form.handleSubmit(
onSubmit
)}
>
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpClientId")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpClientIdDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clientSecret"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"idpClientSecret"
)}
</FormLabel>
<FormControl>
<Input
type="password"
{...field}
/>
</FormControl>
<FormDescription>
{t(
"idpClientSecretDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="authUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpAuthUrl")}
</FormLabel>
<FormControl>
<Input
placeholder="https://your-idp.com/oauth2/authorize"
{...field}
/>
</FormControl>
<FormDescription>
{t(
"idpAuthUrlDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tokenUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("idpTokenUrl")}
</FormLabel>
<FormControl>
<Input
placeholder="https://your-idp.com/oauth2/token"
{...field}
/>
</FormControl>
<FormDescription>
{t(
"idpTokenUrlDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("idpToken")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("idpTokenDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<Form {...form}>
<form
className="space-y-4"
id="create-idp-form"
onSubmit={form.handleSubmit(
onSubmit
)}
>
<FormField
control={form.control}
name="identifierPath"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"idpJmespathLabel"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpJmespathLabelDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="emailPath"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"idpJmespathEmailPathOptional"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpJmespathEmailPathOptionalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="namePath"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"idpJmespathNamePathOptional"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpJmespathNamePathOptionalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="scopes"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"idpOidcConfigureScopes"
)}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"idpOidcConfigureScopesDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionBody>
</SettingsSection>
</SettingsSectionGrid>
)}
</fieldset> </fieldset>
</SettingsContainer> </SettingsContainer>

View File

@@ -1,14 +1,12 @@
"use client"; "use client";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import { FormDescription } from "@app/components/ui/form"; import { FormDescription } from "@app/components/ui/form";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { import { MappingBuilderRule, RoleMappingMode } from "@app/lib/idpRoleMapping";
MappingBuilderRule,
RoleMappingMode
} from "@app/lib/idpRoleMapping";
import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields"; import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields";
type Role = { type Role = {
@@ -60,9 +58,6 @@ export default function AutoProvisionConfigWidget({
onCheckedChange={onAutoProvisionChange} onCheckedChange={onAutoProvisionChange}
disabled={!isPaidUser(tierMatrix.autoProvisioning)} disabled={!isPaidUser(tierMatrix.autoProvisioning)}
/> />
<FormDescription className="text-sm text-muted-foreground">
{t("idpAutoProvisionUsersDescription")}
</FormDescription>
</div> </div>
{autoProvision && ( {autoProvision && (

View File

@@ -0,0 +1,29 @@
"use client";
import { useTranslations } from "next-intl";
const AUTO_PROVISION_DOCS_URL =
"https://docs.pangolin.net/manage/identity-providers/auto-provisioning";
type IdpAutoProvisionUsersDescriptionProps = {
className?: string;
};
export default function IdpAutoProvisionUsersDescription({
className
}: IdpAutoProvisionUsersDescriptionProps) {
const t = useTranslations();
return (
<span className={className}>
{t("idpAutoProvisionUsersDescription")}{" "}
<a
href={AUTO_PROVISION_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t("learnMore")}
</a>
</span>
);
}

View File

@@ -27,6 +27,7 @@ import { Checkbox } from "@app/components/ui/checkbox";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon, ExternalLink } from "lucide-react"; import { InfoIcon, ExternalLink } from "lucide-react";
import { StrategySelect } from "@app/components/StrategySelect"; import { StrategySelect } from "@app/components/StrategySelect";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { Badge } from "@app/components/ui/badge"; import { Badge } from "@app/components/ui/badge";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -163,9 +164,6 @@ export function IdpCreateWizard({
disabled={loading} disabled={loading}
/> />
</div> </div>
<span className="text-sm text-muted-foreground">
{t("idpAutoProvisionUsersDescription")}
</span>
</form> </form>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>