mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-18 02:46:37 +00:00
✨ approval feed
This commit is contained in:
@@ -2,23 +2,25 @@
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { formatFingerprintInfo } from "@app/lib/formatDeviceFingerprint";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import {
|
||||
approvalFiltersSchema,
|
||||
approvalQueries,
|
||||
type ApprovalItem
|
||||
} from "@app/lib/queries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowRight, Ban, Check, LaptopMinimal, RefreshCw } from "lucide-react";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { Ban, Check, Loader, RefreshCw } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Fragment, useActionState } from "react";
|
||||
import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { Card, CardHeader } from "./ui/card";
|
||||
import { InfoPopup } from "./ui/info-popup";
|
||||
import { Label } from "./ui/label";
|
||||
import {
|
||||
Select,
|
||||
@@ -28,8 +30,6 @@ import {
|
||||
SelectValue
|
||||
} from "./ui/select";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { InfoPopup } from "./ui/info-popup";
|
||||
import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
|
||||
|
||||
export type ApprovalFeedProps = {
|
||||
orgId: string;
|
||||
@@ -50,11 +50,17 @@ export function ApprovalFeed({
|
||||
Object.fromEntries(searchParams.entries())
|
||||
);
|
||||
|
||||
const { data, isFetching, refetch } = useQuery(
|
||||
approvalQueries.listApprovals(orgId, filters)
|
||||
);
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery(approvalQueries.listApprovals(orgId, filters));
|
||||
|
||||
const approvals = data?.approvals ?? [];
|
||||
const approvals = data?.pages.flatMap((data) => data.approvals) ?? [];
|
||||
|
||||
// Show empty state if no approvals are enabled for any role
|
||||
if (!hasApprovalsEnabled) {
|
||||
@@ -110,13 +116,13 @@ export function ApprovalFeed({
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
disabled={isFetching}
|
||||
disabled={isFetching || isLoading}
|
||||
className="lg:static gap-2"
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn(
|
||||
"size-4",
|
||||
isFetching && "animate-spin"
|
||||
(isFetching || isLoading) && "animate-spin"
|
||||
)}
|
||||
/>
|
||||
{t("refresh")}
|
||||
@@ -140,13 +146,30 @@ export function ApprovalFeed({
|
||||
))}
|
||||
|
||||
{approvals.length === 0 && (
|
||||
<li className="flex justify-center items-center p-4 text-muted-foreground">
|
||||
{t("approvalListEmpty")}
|
||||
<li className="flex justify-center items-center p-4 text-muted-foreground gap-2">
|
||||
{isLoading
|
||||
? t("loadingApprovals")
|
||||
: t("approvalListEmpty")}
|
||||
|
||||
{isLoading && (
|
||||
<Loader className="size-4 flex-none animate-spin" />
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="self-center"
|
||||
size="lg"
|
||||
loading={isFetchingNextPage}
|
||||
onClick={() => fetchNextPage()}
|
||||
>
|
||||
{isFetchingNextPage ? t("loading") : t("approvalLoadMore")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -209,19 +232,19 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
||||
|
||||
{approval.type === "user_device" && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{approval.deviceName ? (
|
||||
<>
|
||||
{t("requestingNewDeviceApproval")}:{" "}
|
||||
{approval.niceId ? (
|
||||
<Link
|
||||
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
{approval.deviceName}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{approval.deviceName}</span>
|
||||
)}
|
||||
{approval.deviceName ? (
|
||||
<>
|
||||
{t("requestingNewDeviceApproval")}:{" "}
|
||||
{approval.niceId ? (
|
||||
<Link
|
||||
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
{approval.deviceName}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{approval.deviceName}</span>
|
||||
)}
|
||||
{approval.fingerprint && (
|
||||
<InfoPopup>
|
||||
<div className="space-y-1 text-sm">
|
||||
@@ -229,7 +252,10 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
||||
{t("deviceInformation")}
|
||||
</div>
|
||||
<div className="text-muted-foreground whitespace-pre-line">
|
||||
{formatFingerprintInfo(approval.fingerprint, t)}
|
||||
{formatFingerprintInfo(
|
||||
approval.fingerprint,
|
||||
t
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</InfoPopup>
|
||||
|
||||
Reference in New Issue
Block a user