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