mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-03 16:43:45 +00:00
Merge branch 'dev' into feat/show-newt-install-command
This commit is contained in:
91
src/components/ActionBanner.tsx
Normal file
91
src/components/ActionBanner.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import React, { type ReactNode } from "react";
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
const actionBannerVariants = cva(
|
||||
"mb-6 relative overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
warning: "border-yellow-500/30 bg-gradient-to-br from-yellow-500/10 via-background to-background",
|
||||
info: "border-blue-500/30 bg-gradient-to-br from-blue-500/10 via-background to-background",
|
||||
success: "border-green-500/30 bg-gradient-to-br from-green-500/10 via-background to-background",
|
||||
destructive: "border-red-500/30 bg-gradient-to-br from-red-500/10 via-background to-background",
|
||||
default: "border-primary/30 bg-gradient-to-br from-primary/10 via-background to-background"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const titleVariants = "text-lg font-semibold flex items-center gap-2";
|
||||
|
||||
const iconVariants = cva(
|
||||
"w-5 h-5",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
warning: "text-yellow-600 dark:text-yellow-500",
|
||||
info: "text-blue-600 dark:text-blue-500",
|
||||
success: "text-green-600 dark:text-green-500",
|
||||
destructive: "text-red-600 dark:text-red-500",
|
||||
default: "text-primary"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
type ActionBannerProps = {
|
||||
title: string;
|
||||
titleIcon?: ReactNode;
|
||||
description: string;
|
||||
actions?: ReactNode;
|
||||
className?: string;
|
||||
} & VariantProps<typeof actionBannerVariants>;
|
||||
|
||||
export function ActionBanner({
|
||||
title,
|
||||
titleIcon,
|
||||
description,
|
||||
actions,
|
||||
variant = "default",
|
||||
className
|
||||
}: ActionBannerProps) {
|
||||
return (
|
||||
<Card className={cn(actionBannerVariants({ variant }), className)}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center gap-6">
|
||||
<div className="flex-1 space-y-2 min-w-0">
|
||||
<h3 className={titleVariants}>
|
||||
{titleIcon && (
|
||||
<span className={cn(iconVariants({ variant }))}>
|
||||
{titleIcon}
|
||||
</span>
|
||||
)}
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-4xl">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="flex flex-wrap gap-3 lg:shrink-0 lg:justify-end">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionBanner;
|
||||
@@ -19,7 +19,11 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<InfoSections cols={3}>
|
||||
<InfoSections cols={4}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("name")}</InfoSectionTitle>
|
||||
<InfoSectionContent>{client.name}</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
|
||||
<InfoSectionContent>{client.niceId}</InfoSectionContent>
|
||||
|
||||
@@ -161,7 +161,7 @@ export default function CreateRoleForm({
|
||||
)}
|
||||
/>
|
||||
{build !== "oss" && (
|
||||
<div className="pt-3">
|
||||
<div>
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<FormField
|
||||
|
||||
@@ -166,7 +166,7 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||
return (
|
||||
<CredenzaFooter
|
||||
className={cn(
|
||||
"mt-8 md:mt-0 -mx-6 px-6 py-4 border-t border-border",
|
||||
"mt-8 md:mt-0 -mx-6 -mb-4 px-6 py-4 border-t border-border",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -57,8 +57,8 @@ export default function DashboardLoginForm({
|
||||
? env.branding.logo?.authPage?.width || 175
|
||||
: 175;
|
||||
const logoHeight = isUnlocked()
|
||||
? env.branding.logo?.authPage?.height || 58
|
||||
: 58;
|
||||
? env.branding.logo?.authPage?.height || 44
|
||||
: 44;
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
|
||||
@@ -195,8 +195,8 @@ export default function DeviceLoginForm({
|
||||
? env.branding.logo?.authPage?.width || 175
|
||||
: 175;
|
||||
const logoHeight = isUnlocked()
|
||||
? env.branding.logo?.authPage?.height || 58
|
||||
: 58;
|
||||
? env.branding.logo?.authPage?.height || 44
|
||||
: 44;
|
||||
|
||||
function onCancel() {
|
||||
setMetadata(null);
|
||||
|
||||
@@ -169,7 +169,7 @@ export default function EditRoleForm({
|
||||
)}
|
||||
/>
|
||||
{build !== "oss" && (
|
||||
<div className="pt-3">
|
||||
<div>
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<FormField
|
||||
|
||||
@@ -11,7 +11,7 @@ export function InfoSections({
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`grid md:grid-cols-(--columns) md:gap-4 gap-2 md:items-start grid-cols-1`}
|
||||
className={`grid grid-cols-2 md:grid-cols-(--columns) md:gap-4 gap-2 md:items-start`}
|
||||
style={{
|
||||
// @ts-expect-error dynamic props don't work with tailwind, but we can set the
|
||||
// value of a CSS variable at runtime and tailwind will just reuse that value
|
||||
|
||||
@@ -17,8 +17,8 @@ export default function LoginCardHeader({ subtitle }: LoginCardHeaderProps) {
|
||||
? env.branding.logo?.authPage?.width || 175
|
||||
: 175;
|
||||
const logoHeight = isUnlocked()
|
||||
? env.branding.logo?.authPage?.height || 58
|
||||
: 58;
|
||||
? env.branding.logo?.authPage?.height || 44
|
||||
: 44;
|
||||
|
||||
return (
|
||||
<CardHeader className="border-b">
|
||||
|
||||
@@ -59,7 +59,6 @@ export default function MachineClientsTable({
|
||||
const t = useTranslations();
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
|
||||
const [selectedClient, setSelectedClient] = useState<ClientRow | null>(
|
||||
null
|
||||
);
|
||||
@@ -152,8 +151,6 @@ export default function MachineClientsTable({
|
||||
.then(() => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
setIsBlockModalOpen(false);
|
||||
setSelectedClient(null);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -421,8 +418,7 @@ export default function MachineClientsTable({
|
||||
if (clientRow.blocked) {
|
||||
unblockClient(clientRow.id);
|
||||
} else {
|
||||
setSelectedClient(clientRow);
|
||||
setIsBlockModalOpen(true);
|
||||
blockClient(clientRow.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -482,28 +478,6 @@ export default function MachineClientsTable({
|
||||
title="Delete Client"
|
||||
/>
|
||||
)}
|
||||
{selectedClient && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isBlockModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsBlockModalOpen(val);
|
||||
if (!val) {
|
||||
setSelectedClient(null);
|
||||
}
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>{t("blockClientQuestion")}</p>
|
||||
<p>{t("blockClientMessage")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("blockClientConfirm")}
|
||||
onConfirm={async () => blockClient(selectedClient!.id)}
|
||||
string={selectedClient.name}
|
||||
title={t("blockClient")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={machineClients || []}
|
||||
|
||||
@@ -42,8 +42,8 @@ export function OrgSelectionForm() {
|
||||
? env.branding.logo?.authPage?.width || 175
|
||||
: 175;
|
||||
const logoHeight = isUnlocked()
|
||||
? env.branding.logo?.authPage?.height || 58
|
||||
: 58;
|
||||
? env.branding.logo?.authPage?.height || 44
|
||||
: 44;
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -201,8 +201,8 @@ export default function SignupForm({
|
||||
? env.branding.logo?.authPage?.width || 175
|
||||
: 175;
|
||||
const logoHeight = isUnlocked()
|
||||
? env.branding.logo?.authPage?.height || 58
|
||||
: 58;
|
||||
? env.branding.logo?.authPage?.height || 44
|
||||
: 44;
|
||||
|
||||
const showOrgBanner = fromSmartLogin && (build === "saas" || env.flags.useOrgOnlyIdp);
|
||||
const orgBannerHref = redirect
|
||||
|
||||
@@ -27,7 +27,7 @@ import ClientDownloadBanner from "./ClientDownloadBanner";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { build } from "@server/build";
|
||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||
import { t } from "@faker-js/faker/dist/airline-DF6RqYmq";
|
||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
|
||||
export type ClientRow = {
|
||||
id: number;
|
||||
@@ -48,6 +48,16 @@ export type ClientRow = {
|
||||
approvalState: "approved" | "pending" | "denied" | null;
|
||||
archived?: boolean;
|
||||
blocked?: boolean;
|
||||
fingerprint?: {
|
||||
platform: string | null;
|
||||
osVersion: string | null;
|
||||
kernelVersion: string | null;
|
||||
arch: string | null;
|
||||
deviceModel: string | null;
|
||||
serialNumber: string | null;
|
||||
username: string | null;
|
||||
hostname: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type ClientTableProps = {
|
||||
@@ -55,12 +65,57 @@ type ClientTableProps = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
function formatPlatform(platform: string | null | undefined): string {
|
||||
if (!platform) return "-";
|
||||
const platformMap: Record<string, string> = {
|
||||
macos: "macOS",
|
||||
windows: "Windows",
|
||||
linux: "Linux",
|
||||
ios: "iOS",
|
||||
android: "Android",
|
||||
unknown: "Unknown"
|
||||
};
|
||||
return platformMap[platform.toLowerCase()] || platform;
|
||||
}
|
||||
|
||||
export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
|
||||
const formatFingerprintInfo = (
|
||||
fingerprint: ClientRow["fingerprint"]
|
||||
): string => {
|
||||
if (!fingerprint) return "";
|
||||
const parts: string[] = [];
|
||||
|
||||
if (fingerprint.platform) {
|
||||
parts.push(
|
||||
`${t("platform")}: ${formatPlatform(fingerprint.platform)}`
|
||||
);
|
||||
}
|
||||
if (fingerprint.deviceModel) {
|
||||
parts.push(`${t("deviceModel")}: ${fingerprint.deviceModel}`);
|
||||
}
|
||||
if (fingerprint.osVersion) {
|
||||
parts.push(`${t("osVersion")}: ${fingerprint.osVersion}`);
|
||||
}
|
||||
if (fingerprint.arch) {
|
||||
parts.push(`${t("architecture")}: ${fingerprint.arch}`);
|
||||
}
|
||||
if (fingerprint.hostname) {
|
||||
parts.push(`${t("hostname")}: ${fingerprint.hostname}`);
|
||||
}
|
||||
if (fingerprint.username) {
|
||||
parts.push(`${t("username")}: ${fingerprint.username}`);
|
||||
}
|
||||
if (fingerprint.serialNumber) {
|
||||
parts.push(`${t("serialNumber")}: ${fingerprint.serialNumber}`);
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
};
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isBlockModalOpen, setIsBlockModalOpen] = useState(false);
|
||||
const [selectedClient, setSelectedClient] = useState<ClientRow | null>(
|
||||
null
|
||||
);
|
||||
@@ -152,8 +207,6 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
.then(() => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
setIsBlockModalOpen(false);
|
||||
setSelectedClient(null);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -185,7 +238,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
{
|
||||
accessorKey: "name",
|
||||
enableHiding: false,
|
||||
friendlyName: "Name",
|
||||
friendlyName: t("name"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -196,16 +249,31 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
Name
|
||||
{t("name")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const fingerprintInfo = r.fingerprint
|
||||
? formatFingerprintInfo(r.fingerprint)
|
||||
: null;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{r.name}</span>
|
||||
{fingerprintInfo && (
|
||||
<InfoPopup>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="font-semibold mb-2">
|
||||
{t("deviceInformation")}
|
||||
</div>
|
||||
<div className="text-muted-foreground whitespace-pre-line">
|
||||
{fingerprintInfo}
|
||||
</div>
|
||||
</div>
|
||||
</InfoPopup>
|
||||
)}
|
||||
{r.archived && (
|
||||
<Badge variant="secondary">
|
||||
{t("archived")}
|
||||
@@ -253,7 +321,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "userEmail",
|
||||
friendlyName: "User",
|
||||
friendlyName: t("users"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -264,7 +332,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
User
|
||||
{t("users")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -287,7 +355,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "online",
|
||||
friendlyName: "Connectivity",
|
||||
friendlyName: t("online"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -298,7 +366,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
Connectivity
|
||||
{t("online")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -309,14 +377,14 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
return (
|
||||
<span className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Connected</span>
|
||||
<span>{t("online")}</span>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="text-neutral-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>Disconnected</span>
|
||||
<span>{t("offline")}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -324,7 +392,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "mbIn",
|
||||
friendlyName: "Data In",
|
||||
friendlyName: t("dataIn"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -335,7 +403,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
Data In
|
||||
{t("dataIn")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -343,7 +411,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "mbOut",
|
||||
friendlyName: "Data Out",
|
||||
friendlyName: t("dataOut"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -354,7 +422,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
Data Out
|
||||
{t("dataOut")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -402,7 +470,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "subnet",
|
||||
friendlyName: "Address",
|
||||
friendlyName: t("address"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -413,7 +481,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
)
|
||||
}
|
||||
>
|
||||
Address
|
||||
{t("address")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -448,8 +516,8 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
>
|
||||
<span>
|
||||
{clientRow.archived
|
||||
? "Unarchive"
|
||||
: "Archive"}
|
||||
? t("actionUnarchiveClient")
|
||||
: t("actionArchiveClient")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -457,15 +525,14 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
if (clientRow.blocked) {
|
||||
unblockClient(clientRow.id);
|
||||
} else {
|
||||
setSelectedClient(clientRow);
|
||||
setIsBlockModalOpen(true);
|
||||
blockClient(clientRow.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{clientRow.blocked
|
||||
? "Unblock"
|
||||
: "Block"}
|
||||
? t("actionUnblockClient")
|
||||
: t("actionBlockClient")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{!clientRow.userId && (
|
||||
@@ -477,17 +544,17 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
Delete
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
||||
href={`/${clientRow.orgId}/settings/clients/user/${clientRow.niceId}`}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
View
|
||||
{t("viewDetails")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -499,6 +566,49 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
return baseColumns;
|
||||
}, [hasRowsWithoutUserId, t]);
|
||||
|
||||
const statusFilterOptions = useMemo(() => {
|
||||
const allOptions = [
|
||||
{
|
||||
id: "active",
|
||||
label: t("active"),
|
||||
value: "active"
|
||||
},
|
||||
{
|
||||
id: "pending",
|
||||
label: t("pendingApproval"),
|
||||
value: "pending"
|
||||
},
|
||||
{
|
||||
id: "denied",
|
||||
label: t("deniedApproval"),
|
||||
value: "denied"
|
||||
},
|
||||
{
|
||||
id: "archived",
|
||||
label: t("archived"),
|
||||
value: "archived"
|
||||
},
|
||||
{
|
||||
id: "blocked",
|
||||
label: t("blocked"),
|
||||
value: "blocked"
|
||||
}
|
||||
];
|
||||
|
||||
if (build === "oss") {
|
||||
return allOptions.filter((option) => option.value !== "pending");
|
||||
}
|
||||
|
||||
return allOptions;
|
||||
}, [t]);
|
||||
|
||||
const statusFilterDefaultValues = useMemo(() => {
|
||||
if (build === "oss") {
|
||||
return ["active"];
|
||||
}
|
||||
return ["active", "pending"];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedClient && !selectedClient.userId && (
|
||||
@@ -514,34 +624,12 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
<p>{t("clientMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText="Confirm Delete Client"
|
||||
buttonText={t("actionDeleteClient")}
|
||||
onConfirm={async () => deleteClient(selectedClient!.id)}
|
||||
string={selectedClient.name}
|
||||
title="Delete Client"
|
||||
title={t("actionDeleteClient")}
|
||||
/>
|
||||
)}
|
||||
{selectedClient && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isBlockModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsBlockModalOpen(val);
|
||||
if (!val) {
|
||||
setSelectedClient(null);
|
||||
}
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>{t("blockClientQuestion")}</p>
|
||||
<p>{t("blockClientMessage")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("blockClientConfirm")}
|
||||
onConfirm={async () => blockClient(selectedClient!.id)}
|
||||
string={selectedClient.name}
|
||||
title={t("blockClient")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ClientDownloadBanner />
|
||||
|
||||
<DataTable
|
||||
@@ -563,33 +651,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
label: t("status") || "Status",
|
||||
multiSelect: true,
|
||||
displayMode: "calculated",
|
||||
options: [
|
||||
{
|
||||
id: "active",
|
||||
label: t("active"),
|
||||
value: "active"
|
||||
},
|
||||
{
|
||||
id: "pending",
|
||||
label: t("pendingApproval"),
|
||||
value: "pending"
|
||||
},
|
||||
{
|
||||
id: "denied",
|
||||
label: t("deniedApproval"),
|
||||
value: "denied"
|
||||
},
|
||||
{
|
||||
id: "archived",
|
||||
label: t("archived"),
|
||||
value: "archived"
|
||||
},
|
||||
{
|
||||
id: "blocked",
|
||||
label: t("blocked"),
|
||||
value: "blocked"
|
||||
}
|
||||
],
|
||||
options: statusFilterOptions,
|
||||
filterFn: (
|
||||
row: ClientRow,
|
||||
selectedValues: (string | number | boolean)[]
|
||||
@@ -598,7 +660,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
const rowArchived = row.archived;
|
||||
const rowBlocked = row.blocked;
|
||||
const approvalState = row.approvalState;
|
||||
const isActive = !rowArchived && !rowBlocked;
|
||||
const isActive = !rowArchived && !rowBlocked && approvalState !== "pending" && approvalState !== "denied";
|
||||
|
||||
if (selectedValues.includes("active") && isActive)
|
||||
return true;
|
||||
@@ -624,7 +686,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
defaultValues: ["active", "pending"] // Default to showing active clients
|
||||
defaultValues: statusFilterDefaultValues
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -133,6 +133,7 @@ export default function IdpLoginButtons({
|
||||
loginWithIdp(idp.idpId);
|
||||
}}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
{effectiveType === "google" && (
|
||||
<Image
|
||||
|
||||
@@ -11,7 +11,7 @@ const alertVariants = cva(
|
||||
default: "bg-card border text-foreground",
|
||||
neutral: "bg-card bg-muted border text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 border bg-destructive/8 text-destructive dark:border-destructive/50 [&>svg]:text-destructive",
|
||||
"border-destructive/50 border bg-destructive/8 dark:text-red-200 text-red-900 dark:border-destructive/50 [&>svg]:text-destructive",
|
||||
success:
|
||||
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
||||
info: "border-blue-500/50 border bg-blue-500/10 text-blue-800 dark:text-blue-400 dark:border-blue-400 [&>svg]:text-blue-500",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Info } from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
@@ -17,25 +17,61 @@ interface InfoPopupProps {
|
||||
}
|
||||
|
||||
export function InfoPopup({ text, info, trigger, children }: InfoPopupProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
// Add a small delay to prevent flickering when moving between trigger and content
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setOpen(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const defaultTrigger = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 rounded-full p-0"
|
||||
className="h-6 w-6 rounded-full p-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
<span className="sr-only">Show info</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const triggerElement = trigger ?? defaultTrigger;
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{text && <span>{text}</span>}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
{trigger ?? defaultTrigger}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{triggerElement}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<PopoverContent
|
||||
className="w-80"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{children ||
|
||||
(info && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user