mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-06 02:36:38 +00:00
✨ approval feed
This commit is contained in:
@@ -459,6 +459,8 @@
|
|||||||
"filterByApprovalState": "Filter By Approval State",
|
"filterByApprovalState": "Filter By Approval State",
|
||||||
"approvalListEmpty": "No approvals",
|
"approvalListEmpty": "No approvals",
|
||||||
"approvalState": "Approval State",
|
"approvalState": "Approval State",
|
||||||
|
"approvalLoadMore": "Load more",
|
||||||
|
"loadingApprovals": "Loading Approvals",
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
"approved": "Approved",
|
"approved": "Approved",
|
||||||
"denied": "Denied",
|
"denied": "Denied",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
currentFingerprint,
|
currentFingerprint,
|
||||||
type Approval
|
type Approval
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, isNull, sql, not, and, desc } from "drizzle-orm";
|
import { eq, isNull, sql, not, and, desc, gte, lte } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { getUserDeviceName } from "@server/db/names";
|
import { getUserDeviceName } from "@server/db/names";
|
||||||
|
|
||||||
@@ -39,18 +39,26 @@ const paramsSchema = z.strictObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const querySchema = z.strictObject({
|
const querySchema = z.strictObject({
|
||||||
limit: z
|
limit: z.coerce
|
||||||
.string()
|
.number<string>() // for prettier formatting
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
.optional()
|
.optional()
|
||||||
.default("1000")
|
.catch(20)
|
||||||
.transform(Number)
|
.default(20),
|
||||||
.pipe(z.int().nonnegative()),
|
cursorPending: z.coerce // pending cursor
|
||||||
offset: z
|
.number<string>()
|
||||||
.string()
|
.int()
|
||||||
|
.max(1) // 0 means non pending
|
||||||
|
.min(0) // 1 means pending
|
||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.catch(undefined),
|
||||||
.transform(Number)
|
cursorTimestamp: z.coerce
|
||||||
.pipe(z.int().nonnegative()),
|
.number<string>()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.optional()
|
||||||
|
.catch(undefined),
|
||||||
approvalState: z
|
approvalState: z
|
||||||
.enum(["pending", "approved", "denied", "all"])
|
.enum(["pending", "approved", "denied", "all"])
|
||||||
.optional()
|
.optional()
|
||||||
@@ -63,13 +71,21 @@ const querySchema = z.strictObject({
|
|||||||
.pipe(z.number().int().positive().optional())
|
.pipe(z.number().int().positive().optional())
|
||||||
});
|
});
|
||||||
|
|
||||||
async function queryApprovals(
|
async function queryApprovals({
|
||||||
orgId: string,
|
orgId,
|
||||||
limit: number,
|
limit,
|
||||||
offset: number,
|
approvalState,
|
||||||
approvalState: z.infer<typeof querySchema>["approvalState"],
|
cursorPending,
|
||||||
clientId?: number
|
cursorTimestamp,
|
||||||
) {
|
clientId
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
limit: number;
|
||||||
|
approvalState: z.infer<typeof querySchema>["approvalState"];
|
||||||
|
cursorPending?: number;
|
||||||
|
cursorTimestamp?: number;
|
||||||
|
clientId?: number;
|
||||||
|
}) {
|
||||||
let state: Array<Approval["decision"]> = [];
|
let state: Array<Approval["decision"]> = [];
|
||||||
switch (approvalState) {
|
switch (approvalState) {
|
||||||
case "pending":
|
case "pending":
|
||||||
@@ -85,6 +101,26 @@ async function queryApprovals(
|
|||||||
state = ["approved", "denied", "pending"];
|
state = ["approved", "denied", "pending"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const conditions = [
|
||||||
|
eq(approvals.orgId, orgId),
|
||||||
|
sql`${approvals.decision} in ${state}`
|
||||||
|
];
|
||||||
|
|
||||||
|
if (clientId) {
|
||||||
|
conditions.push(eq(approvals.clientId, clientId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingSortKey = sql`CASE ${approvals.decision} WHEN 'pending' THEN 1 ELSE 0 END`;
|
||||||
|
|
||||||
|
if (cursorPending != null && cursorTimestamp != null) {
|
||||||
|
// https://stackoverflow.com/a/79720298/10322846
|
||||||
|
// composite cursor, next data means (pending, timestamp) <= cursor
|
||||||
|
conditions.push(
|
||||||
|
lte(pendingSortKey, cursorPending),
|
||||||
|
lte(approvals.timestamp, cursorTimestamp)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const res = await db
|
const res = await db
|
||||||
.select({
|
.select({
|
||||||
approvalId: approvals.approvalId,
|
approvalId: approvals.approvalId,
|
||||||
@@ -107,7 +143,8 @@ async function queryApprovals(
|
|||||||
fingerprintArch: currentFingerprint.arch,
|
fingerprintArch: currentFingerprint.arch,
|
||||||
fingerprintSerialNumber: currentFingerprint.serialNumber,
|
fingerprintSerialNumber: currentFingerprint.serialNumber,
|
||||||
fingerprintUsername: currentFingerprint.username,
|
fingerprintUsername: currentFingerprint.username,
|
||||||
fingerprintHostname: currentFingerprint.hostname
|
fingerprintHostname: currentFingerprint.hostname,
|
||||||
|
timestamp: approvals.timestamp
|
||||||
})
|
})
|
||||||
.from(approvals)
|
.from(approvals)
|
||||||
.innerJoin(users, and(eq(approvals.userId, users.userId)))
|
.innerJoin(users, and(eq(approvals.userId, users.userId)))
|
||||||
@@ -120,22 +157,12 @@ async function queryApprovals(
|
|||||||
)
|
)
|
||||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||||
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
|
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
|
||||||
.where(
|
.where(and(...conditions))
|
||||||
and(
|
.orderBy(desc(pendingSortKey), desc(approvals.timestamp))
|
||||||
eq(approvals.orgId, orgId),
|
.limit(limit + 1); // the `+1` is used for the cursor
|
||||||
sql`${approvals.decision} in ${state}`,
|
|
||||||
...(clientId ? [eq(approvals.clientId, clientId)] : [])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.orderBy(
|
|
||||||
sql`CASE ${approvals.decision} WHEN 'pending' THEN 0 ELSE 1 END`,
|
|
||||||
desc(approvals.timestamp)
|
|
||||||
)
|
|
||||||
.limit(limit)
|
|
||||||
.offset(offset);
|
|
||||||
|
|
||||||
// Process results to format device names and build fingerprint objects
|
// Process results to format device names and build fingerprint objects
|
||||||
return res.map((approval) => {
|
const approvalsList = res.slice(0, limit).map((approval) => {
|
||||||
const model = approval.deviceModel || null;
|
const model = approval.deviceModel || null;
|
||||||
const deviceName = approval.clientName
|
const deviceName = approval.clientName
|
||||||
? getUserDeviceName(model, approval.clientName)
|
? getUserDeviceName(model, approval.clientName)
|
||||||
@@ -154,15 +181,15 @@ async function queryApprovals(
|
|||||||
|
|
||||||
const fingerprint = hasFingerprintData
|
const fingerprint = hasFingerprintData
|
||||||
? {
|
? {
|
||||||
platform: approval.fingerprintPlatform || null,
|
platform: approval.fingerprintPlatform ?? null,
|
||||||
osVersion: approval.fingerprintOsVersion || null,
|
osVersion: approval.fingerprintOsVersion ?? null,
|
||||||
kernelVersion: approval.fingerprintKernelVersion || null,
|
kernelVersion: approval.fingerprintKernelVersion ?? null,
|
||||||
arch: approval.fingerprintArch || null,
|
arch: approval.fingerprintArch ?? null,
|
||||||
deviceModel: approval.deviceModel || null,
|
deviceModel: approval.deviceModel ?? null,
|
||||||
serialNumber: approval.fingerprintSerialNumber || null,
|
serialNumber: approval.fingerprintSerialNumber ?? null,
|
||||||
username: approval.fingerprintUsername || null,
|
username: approval.fingerprintUsername ?? null,
|
||||||
hostname: approval.fingerprintHostname || null
|
hostname: approval.fingerprintHostname ?? null
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -185,11 +212,30 @@ async function queryApprovals(
|
|||||||
niceId: approval.niceId || null
|
niceId: approval.niceId || null
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
let nextCursorPending: number | null = null;
|
||||||
|
let nextCursorTimestamp: number | null = null;
|
||||||
|
if (res.length > limit) {
|
||||||
|
const lastItem = res[limit];
|
||||||
|
nextCursorPending = lastItem.decision === "pending" ? 1 : 0;
|
||||||
|
nextCursorTimestamp = lastItem.timestamp;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
approvalsList,
|
||||||
|
nextCursorPending,
|
||||||
|
nextCursorTimestamp
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListApprovalsResponse = {
|
export type ListApprovalsResponse = {
|
||||||
approvals: NonNullable<Awaited<ReturnType<typeof queryApprovals>>>;
|
approvals: NonNullable<
|
||||||
pagination: { total: number; limit: number; offset: number };
|
Awaited<ReturnType<typeof queryApprovals>>
|
||||||
|
>["approvalsList"];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
cursorPending: number | null;
|
||||||
|
cursorTimestamp: number | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function listApprovals(
|
export async function listApprovals(
|
||||||
@@ -217,7 +263,13 @@ export async function listApprovals(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { limit, offset, approvalState, clientId } = parsedQuery.data;
|
const {
|
||||||
|
limit,
|
||||||
|
cursorPending,
|
||||||
|
cursorTimestamp,
|
||||||
|
approvalState,
|
||||||
|
clientId
|
||||||
|
} = parsedQuery.data;
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
@@ -234,13 +286,15 @@ export async function listApprovals(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const approvalsList = await queryApprovals(
|
const { approvalsList, nextCursorPending, nextCursorTimestamp } =
|
||||||
orgId.toString(),
|
await queryApprovals({
|
||||||
limit,
|
orgId: orgId.toString(),
|
||||||
offset,
|
limit,
|
||||||
approvalState,
|
cursorPending,
|
||||||
clientId
|
cursorTimestamp,
|
||||||
);
|
approvalState,
|
||||||
|
clientId
|
||||||
|
});
|
||||||
|
|
||||||
const [{ count }] = await db
|
const [{ count }] = await db
|
||||||
.select({ count: sql<number>`count(*)` })
|
.select({ count: sql<number>`count(*)` })
|
||||||
@@ -252,7 +306,8 @@ export async function listApprovals(
|
|||||||
pagination: {
|
pagination: {
|
||||||
total: count,
|
total: count,
|
||||||
limit,
|
limit,
|
||||||
offset
|
cursorPending: nextCursorPending,
|
||||||
|
cursorTimestamp: nextCursorTimestamp
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
olms,
|
olms,
|
||||||
orgs,
|
orgs,
|
||||||
roleClients,
|
roleClients,
|
||||||
sites,
|
|
||||||
userClients,
|
userClients,
|
||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
@@ -25,7 +24,6 @@ import {
|
|||||||
inArray,
|
inArray,
|
||||||
isNotNull,
|
isNotNull,
|
||||||
isNull,
|
isNull,
|
||||||
not,
|
|
||||||
or,
|
or,
|
||||||
sql,
|
sql,
|
||||||
type SQL
|
type SQL
|
||||||
|
|||||||
@@ -2,23 +2,25 @@
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { formatFingerprintInfo } from "@app/lib/formatDeviceFingerprint";
|
import { formatFingerprintInfo } from "@app/lib/formatDeviceFingerprint";
|
||||||
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
import {
|
import {
|
||||||
approvalFiltersSchema,
|
approvalFiltersSchema,
|
||||||
approvalQueries,
|
approvalQueries,
|
||||||
type ApprovalItem
|
type ApprovalItem
|
||||||
} from "@app/lib/queries";
|
} from "@app/lib/queries";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { ArrowRight, Ban, Check, LaptopMinimal, RefreshCw } from "lucide-react";
|
import { Ban, Check, Loader, RefreshCw } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Fragment, useActionState } from "react";
|
import { Fragment, useActionState } from "react";
|
||||||
|
import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Card, CardHeader } from "./ui/card";
|
import { Card, CardHeader } from "./ui/card";
|
||||||
|
import { InfoPopup } from "./ui/info-popup";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -28,8 +30,6 @@ import {
|
|||||||
SelectValue
|
SelectValue
|
||||||
} from "./ui/select";
|
} from "./ui/select";
|
||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
import { InfoPopup } from "./ui/info-popup";
|
|
||||||
import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
|
|
||||||
|
|
||||||
export type ApprovalFeedProps = {
|
export type ApprovalFeedProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -50,11 +50,17 @@ export function ApprovalFeed({
|
|||||||
Object.fromEntries(searchParams.entries())
|
Object.fromEntries(searchParams.entries())
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isFetching, refetch } = useQuery(
|
const {
|
||||||
approvalQueries.listApprovals(orgId, filters)
|
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
|
// Show empty state if no approvals are enabled for any role
|
||||||
if (!hasApprovalsEnabled) {
|
if (!hasApprovalsEnabled) {
|
||||||
@@ -110,13 +116,13 @@ export function ApprovalFeed({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
disabled={isFetching}
|
disabled={isFetching || isLoading}
|
||||||
className="lg:static gap-2"
|
className="lg:static gap-2"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4",
|
"size-4",
|
||||||
isFetching && "animate-spin"
|
(isFetching || isLoading) && "animate-spin"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{t("refresh")}
|
{t("refresh")}
|
||||||
@@ -140,13 +146,30 @@ export function ApprovalFeed({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{approvals.length === 0 && (
|
{approvals.length === 0 && (
|
||||||
<li className="flex justify-center items-center p-4 text-muted-foreground">
|
<li className="flex justify-center items-center p-4 text-muted-foreground gap-2">
|
||||||
{t("approvalListEmpty")}
|
{isLoading
|
||||||
|
? t("loadingApprovals")
|
||||||
|
: t("approvalListEmpty")}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<Loader className="size-4 flex-none animate-spin" />
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
|
{hasNextPage && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="self-center"
|
||||||
|
size="lg"
|
||||||
|
loading={isFetchingNextPage}
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? t("loading") : t("approvalLoadMore")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -209,19 +232,19 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
|||||||
|
|
||||||
{approval.type === "user_device" && (
|
{approval.type === "user_device" && (
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
{approval.deviceName ? (
|
{approval.deviceName ? (
|
||||||
<>
|
<>
|
||||||
{t("requestingNewDeviceApproval")}:{" "}
|
{t("requestingNewDeviceApproval")}:{" "}
|
||||||
{approval.niceId ? (
|
{approval.niceId ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
|
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
|
||||||
className="text-primary hover:underline cursor-pointer"
|
className="text-primary hover:underline cursor-pointer"
|
||||||
>
|
>
|
||||||
{approval.deviceName}
|
{approval.deviceName}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span>{approval.deviceName}</span>
|
<span>{approval.deviceName}</span>
|
||||||
)}
|
)}
|
||||||
{approval.fingerprint && (
|
{approval.fingerprint && (
|
||||||
<InfoPopup>
|
<InfoPopup>
|
||||||
<div className="space-y-1 text-sm">
|
<div className="space-y-1 text-sm">
|
||||||
@@ -229,7 +252,10 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
|||||||
{t("deviceInformation")}
|
{t("deviceInformation")}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground whitespace-pre-line">
|
<div className="text-muted-foreground whitespace-pre-line">
|
||||||
{formatFingerprintInfo(approval.fingerprint, t)}
|
{formatFingerprintInfo(
|
||||||
|
approval.fingerprint,
|
||||||
|
t
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</InfoPopup>
|
</InfoPopup>
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ import type {
|
|||||||
import type { ListTargetsResponse } from "@server/routers/target";
|
import type { ListTargetsResponse } from "@server/routers/target";
|
||||||
import type { ListUsersResponse } from "@server/routers/user";
|
import type { ListUsersResponse } from "@server/routers/user";
|
||||||
import type ResponseT from "@server/types/Response";
|
import type ResponseT from "@server/types/Response";
|
||||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
import {
|
||||||
|
infiniteQueryOptions,
|
||||||
|
keepPreviousData,
|
||||||
|
queryOptions
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { remote } from "./api";
|
import { remote } from "./api";
|
||||||
import { durationToMs } from "./durationToMs";
|
import { durationToMs } from "./durationToMs";
|
||||||
|
import { wait } from "./wait";
|
||||||
|
|
||||||
export type ProductUpdate = {
|
export type ProductUpdate = {
|
||||||
link: string | null;
|
link: string | null;
|
||||||
@@ -356,22 +361,50 @@ export const approvalQueries = {
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
filters: z.infer<typeof approvalFiltersSchema>
|
filters: z.infer<typeof approvalFiltersSchema>
|
||||||
) =>
|
) =>
|
||||||
queryOptions({
|
infiniteQueryOptions({
|
||||||
queryKey: ["APPROVALS", orgId, filters] as const,
|
queryKey: ["APPROVALS", orgId, filters] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, pageParam, meta }) => {
|
||||||
const sp = new URLSearchParams();
|
const sp = new URLSearchParams();
|
||||||
|
|
||||||
if (filters.approvalState) {
|
if (filters.approvalState) {
|
||||||
sp.set("approvalState", filters.approvalState);
|
sp.set("approvalState", filters.approvalState);
|
||||||
}
|
}
|
||||||
|
if (pageParam) {
|
||||||
|
sp.set("cursorPending", pageParam.cursorPending.toString());
|
||||||
|
sp.set(
|
||||||
|
"cursorTimestamp",
|
||||||
|
pageParam.cursorTimestamp.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<{ approvals: ApprovalItem[] }>
|
AxiosResponse<{
|
||||||
|
approvals: ApprovalItem[];
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
cursorPending: number | null;
|
||||||
|
cursorTimestamp: number | null;
|
||||||
|
};
|
||||||
|
}>
|
||||||
>(`/org/${orgId}/approvals?${sp.toString()}`, {
|
>(`/org/${orgId}/approvals?${sp.toString()}`, {
|
||||||
signal
|
signal
|
||||||
});
|
});
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
}
|
},
|
||||||
|
initialPageParam: null as {
|
||||||
|
cursorPending: number;
|
||||||
|
cursorTimestamp: number;
|
||||||
|
} | null,
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
getNextPageParam: ({ pagination }) =>
|
||||||
|
pagination.cursorPending != null &&
|
||||||
|
pagination.cursorTimestamp != null
|
||||||
|
? {
|
||||||
|
cursorPending: pagination.cursorPending,
|
||||||
|
cursorTimestamp: pagination.cursorTimestamp
|
||||||
|
}
|
||||||
|
: null
|
||||||
}),
|
}),
|
||||||
pendingCount: (orgId: string) =>
|
pendingCount: (orgId: string) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
|
|||||||
Reference in New Issue
Block a user