mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-11 21:41:41 +02:00
more cosmetic improvememnts
This commit is contained in:
+5
-4
@@ -80,7 +80,7 @@
|
||||
"siteManageSites": "Manage Sites",
|
||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||
"sitesBannerTitle": "Connect Any Network",
|
||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector anywhere you can run a binary or container to establish the connection.",
|
||||
"sitesBannerButtonText": "Install Site Connector",
|
||||
"approvalsBannerTitle": "Approve or Deny Device Access",
|
||||
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
||||
@@ -596,7 +596,7 @@
|
||||
"licensePricingPage": "For the most up-to-date pricing and discounts, please visit the ",
|
||||
"invite": "Invitations",
|
||||
"inviteRegenerate": "Regenerate Invitation",
|
||||
"inviteRegenerateDescription": "Revoke previous invitation and create a new one",
|
||||
"inviteRegenerateDescription": "Create a new invite link for this user. The previous invitation will be revoked.",
|
||||
"inviteRemove": "Remove Invitation",
|
||||
"inviteRemoveError": "Failed to remove invitation",
|
||||
"inviteRemoveErrorDescription": "An error occurred while removing the invitation.",
|
||||
@@ -1356,7 +1356,7 @@
|
||||
"siteLabelsDescription": "Manage labels associated with this site.",
|
||||
"labelsNotFound": "No labels found.",
|
||||
"labelsEmptyCreateHint": "Start typing above to create a label.",
|
||||
"labelSearch": "Search labels",
|
||||
"labelSearch": "Search labels...",
|
||||
"labelSearchOrCreate": "Search or create a label",
|
||||
"accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}",
|
||||
"labelOverflowCount": "+{count, plural, one {# label} other {# labels}}",
|
||||
@@ -3996,11 +3996,12 @@
|
||||
"disconnected": "Disconnected",
|
||||
"approvalsEmptyStateTitle": "Device Approvals Not Enabled",
|
||||
"approvalsEmptyStateDescription": "Enable device approvals for roles to require admin approval before users can connect new devices.",
|
||||
"approvalsEmptyStateHowToTitle": "How to Enable",
|
||||
"approvalsEmptyStateStep1Title": "Go to Roles",
|
||||
"approvalsEmptyStateStep1Description": "Navigate to your organization's roles settings to configure device approvals.",
|
||||
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||
"approvalsEmptyStatePreviewDescription": "When enabled, pending device requests will appear here for review.",
|
||||
"approvalsEmptyStateButtonText": "Manage Roles",
|
||||
"domainErrorTitle": "We are having trouble verifying your domain",
|
||||
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab.",
|
||||
|
||||
@@ -954,7 +954,6 @@ export const ProxyResourceTargetsForm = forwardRef<
|
||||
colSpan={columns.length}
|
||||
message={emptyMessage ?? t("targetNoOne")}
|
||||
action={addTargetButton}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
</TableBody>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import {
|
||||
ShieldCheck,
|
||||
Check,
|
||||
Ban,
|
||||
User,
|
||||
Settings,
|
||||
ArrowRight
|
||||
} from "lucide-react";
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionFooter
|
||||
} from "@app/components/Settings";
|
||||
import { ArrowRight, Settings, ShieldCheck, User } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -21,102 +18,50 @@ export function ApprovalsEmptyState({ orgId }: ApprovalsEmptyStateProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card>
|
||||
<CardContent className="p-6 md:p-12">
|
||||
<div className="flex flex-col items-center text-center gap-4 md:gap-6 max-w-2xl mx-auto">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl md:text-2xl font-semibold">
|
||||
{t("approvalsEmptyStateTitle")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm md:text-lg">
|
||||
{t("approvalsEmptyStateDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<SettingsSection>
|
||||
<SettingsSectionBody>
|
||||
<div className="flex flex-col items-center text-center py-6 md:py-10 px-2">
|
||||
<ShieldCheck className="h-8 w-8 text-primary" />
|
||||
<h2 className="mt-4 text-2xl font-semibold tracking-tight max-w-xl">
|
||||
{t("approvalsEmptyStateTitle")}
|
||||
</h2>
|
||||
<p className="mt-3 text-sm text-muted-foreground max-w-lg">
|
||||
{t("approvalsEmptyStateDescription")}
|
||||
</p>
|
||||
|
||||
<div className="w-full space-y-3 md:space-y-4 mt-2 md:mt-4">
|
||||
<div className="bg-muted/50 rounded-lg p-4 md:p-6 space-y-3 md:space-y-4 border">
|
||||
<div className="flex items-start gap-3 md:gap-4">
|
||||
<div className="rounded-lg bg-background p-2 md:p-3 border shrink-0">
|
||||
<Settings className="w-4 h-4 md:w-5 md:h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 text-left min-w-0">
|
||||
<h4 className="font-semibold mb-1 text-sm md:text-base">
|
||||
{t("approvalsEmptyStateStep1Title")}
|
||||
</h4>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">
|
||||
{t(
|
||||
"approvalsEmptyStateStep1Description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 md:gap-4">
|
||||
<div className="rounded-lg bg-background p-2 md:p-3 border shrink-0">
|
||||
<User className="w-4 h-4 md:w-5 md:h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 text-left min-w-0">
|
||||
<h4 className="font-semibold mb-1 text-sm md:text-base">
|
||||
{t("approvalsEmptyStateStep2Title")}
|
||||
</h4>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">
|
||||
{t(
|
||||
"approvalsEmptyStateStep2Description"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Abstract UI Preview - Hidden on mobile */}
|
||||
<div className="hidden md:block bg-muted/50 rounded-lg p-6 border">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 bg-background rounded border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-3 w-24 bg-muted-foreground/20 rounded mb-1"></div>
|
||||
<div className="h-2 w-32 bg-muted-foreground/10 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="h-6 w-16 bg-muted-foreground/10 rounded"></div>
|
||||
<div className="h-6 w-16 bg-muted-foreground/10 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-background rounded border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-3 w-24 bg-muted-foreground/20 rounded mb-1"></div>
|
||||
<div className="h-2 w-32 bg-muted-foreground/10 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="h-6 w-16 bg-green-500/20 rounded flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-green-600" />
|
||||
</div>
|
||||
<div className="h-6 w-16 bg-muted-foreground/10 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href={`/${orgId}/settings/access/roles`} className="w-full md:w-auto">
|
||||
<Button className="gap-2 mt-2 w-full md:w-auto">
|
||||
{t("approvalsEmptyStateButtonText")}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="mt-8 w-full max-w-lg text-left space-y-3">
|
||||
<p className="text-sm font-medium text-center">
|
||||
{t("approvalsEmptyStateHowToTitle")}
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<Settings className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
||||
<span>
|
||||
{t("approvalsEmptyStateStep1Description")}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<User className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
|
||||
<span>
|
||||
{t("approvalsEmptyStateStep2Description")}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-sm text-muted-foreground max-w-lg">
|
||||
{t("approvalsEmptyStatePreviewDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter className="justify-center md:justify-center">
|
||||
<Button asChild>
|
||||
<Link href={`/${orgId}/settings/access/roles`}>
|
||||
{t("approvalsEmptyStateButtonText")}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,9 +52,11 @@ export default function RegenerateInvitationForm({
|
||||
}: RegenerateInvitationFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inviteLink, setInviteLink] = useState<string | null>(null);
|
||||
const [sendEmail, setSendEmail] = useState(true);
|
||||
const [expiresInDays, setExpiresInDays] = useState(3);
|
||||
const { env } = useEnvContext();
|
||||
const [sendEmail, setSendEmail] = useState(env.email.emailEnabled);
|
||||
const [validHours, setValidHours] = useState(72);
|
||||
const api = createApiClient(useEnvContext());
|
||||
const api = createApiClient({ env });
|
||||
const { org } = useOrgContext();
|
||||
|
||||
const t = useTranslations();
|
||||
@@ -71,10 +73,11 @@ export default function RegenerateInvitationForm({
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSendEmail(true);
|
||||
setSendEmail(env.email.emailEnabled);
|
||||
setValidHours(72);
|
||||
setExpiresInDays(3);
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, env.email.emailEnabled]);
|
||||
|
||||
async function handleRegenerate() {
|
||||
if (!invitation) return;
|
||||
@@ -96,15 +99,16 @@ export default function RegenerateInvitationForm({
|
||||
email: invitation.email,
|
||||
roleIds: invitation.roleIds,
|
||||
validHours,
|
||||
sendEmail,
|
||||
sendEmail: env.email.emailEnabled && sendEmail,
|
||||
regenerate: true
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
const link = res.data.data.inviteLink;
|
||||
setInviteLink(link);
|
||||
setExpiresInDays(validHours / 24);
|
||||
|
||||
if (sendEmail) {
|
||||
if (sendEmail && env.email.emailEnabled) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: t("inviteRegenerated"),
|
||||
@@ -127,9 +131,7 @@ export default function RegenerateInvitationForm({
|
||||
onRegenerate({
|
||||
id: invitation.id,
|
||||
email: invitation.email,
|
||||
expiresAt: new Date(
|
||||
res.data.data.expiresAt
|
||||
).toISOString(),
|
||||
expiresAt: new Date(res.data.data.expiresAt).toISOString(),
|
||||
roleLabels: invitation.roleLabels,
|
||||
roleIds: invitation.roleIds
|
||||
});
|
||||
@@ -174,33 +176,47 @@ export default function RegenerateInvitationForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t("inviteRegenerate")}</CredenzaTitle>
|
||||
<CredenzaTitle>
|
||||
{inviteLink
|
||||
? t("inviteRegenerated")
|
||||
: t("inviteRegenerate")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("inviteRegenerateDescription")}
|
||||
{inviteLink
|
||||
? sendEmail && env.email.emailEnabled
|
||||
? t("inviteEmailSentDescription")
|
||||
: t("inviteSentDescription")
|
||||
: t("inviteRegenerateDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
{!inviteLink ? (
|
||||
<div>
|
||||
<p>
|
||||
{t("inviteQuestionRegenerate", {
|
||||
email: invitation?.email || ""
|
||||
})}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-4">
|
||||
<Checkbox
|
||||
id="send-email"
|
||||
checked={sendEmail}
|
||||
onCheckedChange={(e) =>
|
||||
setSendEmail(e as boolean)
|
||||
}
|
||||
/>
|
||||
<label htmlFor="send-email">
|
||||
{t("inviteSentEmail")}
|
||||
</label>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("email")}</Label>
|
||||
<p className="text-sm">{invitation?.email}</p>
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label>{t("inviteValidityPeriod")}</Label>
|
||||
|
||||
{env.email.emailEnabled && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="send-email"
|
||||
checked={sendEmail}
|
||||
onCheckedChange={(e) =>
|
||||
setSendEmail(e as boolean)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="send-email"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{t("inviteEmailSent")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("inviteValid")}</Label>
|
||||
<Select
|
||||
value={validHours.toString()}
|
||||
onValueChange={(value) =>
|
||||
@@ -209,9 +225,7 @@ export default function RegenerateInvitationForm({
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"inviteValidityPeriodSelect"
|
||||
)}
|
||||
placeholder={t("selectDuration")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -228,9 +242,13 @@ export default function RegenerateInvitationForm({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 max-w-md">
|
||||
<p>{t("inviteRegenerateMessage")}</p>
|
||||
<CopyTextBox text={inviteLink} wrapText={false} />
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t("inviteExpiresIn", {
|
||||
days: expiresInDays
|
||||
})}
|
||||
</p>
|
||||
{inviteLink && <CopyTextBox text={inviteLink} />}
|
||||
</div>
|
||||
)}
|
||||
</CredenzaBody>
|
||||
@@ -249,7 +267,7 @@ export default function RegenerateInvitationForm({
|
||||
</>
|
||||
) : (
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
<Button>{t("done")}</Button>
|
||||
</CredenzaClose>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
|
||||
@@ -961,7 +961,6 @@ export function PolicyAccessRulesTable({
|
||||
colSpan={columns.length}
|
||||
message={t("rulesNoOne")}
|
||||
action={emptyStateAction}
|
||||
compact
|
||||
/>
|
||||
)}
|
||||
</TableBody>
|
||||
|
||||
@@ -5,57 +5,46 @@ import { useTranslations } from "next-intl";
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
const PLACEHOLDER_ROW_COUNT = 5;
|
||||
const COMPACT_PLACEHOLDER_ROW_COUNT = 2;
|
||||
|
||||
type DataTableEmptyStateProps = {
|
||||
colSpan: number;
|
||||
action?: ReactNode;
|
||||
message?: string;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export function DataTableEmptyState({
|
||||
colSpan,
|
||||
action,
|
||||
message,
|
||||
compact = false
|
||||
message
|
||||
}: DataTableEmptyStateProps) {
|
||||
const t = useTranslations();
|
||||
const placeholderRows = compact
|
||||
? COMPACT_PLACEHOLDER_ROW_COUNT
|
||||
: PLACEHOLDER_ROW_COUNT;
|
||||
const minHeightClass = compact ? "min-h-20" : "min-h-[12.5rem]";
|
||||
|
||||
return (
|
||||
<TableRow className="hidden sm:table-row hover:bg-transparent data-[state=selected]:bg-transparent">
|
||||
<TableCell colSpan={colSpan} className="p-0">
|
||||
<div
|
||||
className={`relative w-full overflow-hidden ${minHeightClass}`}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden min-h-[12.5rem]">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 flex flex-col justify-start opacity-50"
|
||||
aria-hidden
|
||||
>
|
||||
{Array.from({ length: placeholderRows }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex h-10 shrink-0 items-center border-b border-border/60 px-4 last:border-b-0"
|
||||
>
|
||||
<div className="h-3.5 w-full rounded bg-muted/60" />
|
||||
</div>
|
||||
))}
|
||||
{Array.from({ length: PLACEHOLDER_ROW_COUNT }).map(
|
||||
(_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex h-10 shrink-0 items-center border-b border-border/60 px-4 last:border-b-0"
|
||||
>
|
||||
<div className="h-3.5 w-full rounded bg-muted/60" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`relative flex w-full flex-col items-center justify-center px-4 ${
|
||||
compact
|
||||
? "min-h-20 gap-3 py-4"
|
||||
: "min-h-[12.5rem] gap-4 py-8"
|
||||
}`}
|
||||
>
|
||||
<div className="relative flex w-full flex-col items-center justify-center px-4 min-h-[12.5rem] gap-4 py-8">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{message ?? t("noResults")}
|
||||
</p>
|
||||
{action}
|
||||
{action ? (
|
||||
<div className="bg-background">{action}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user