mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-11 21:41:41 +02:00
✨ list resources table
This commit is contained in:
+19
-1
@@ -4364,5 +4364,23 @@
|
|||||||
"rdpUnicodeKeyboardMode": "Unicode keyboard mode",
|
"rdpUnicodeKeyboardMode": "Unicode keyboard mode",
|
||||||
"sessionToolbarShow": "Show toolbar",
|
"sessionToolbarShow": "Show toolbar",
|
||||||
"sessionToolbarHide": "Hide toolbar",
|
"sessionToolbarHide": "Hide toolbar",
|
||||||
"actionUpdateSiteApprovals": "Update Site Approvals"
|
"actionUpdateSiteApprovals": "Update Site Approvals",
|
||||||
|
"redirectsTitle": "Manage Redirects",
|
||||||
|
"redirectsDescription": "Forward requests from a path on your domains or resources to another URL",
|
||||||
|
"redirectsSearch": "Search redirects...",
|
||||||
|
"redirectAdd": "Add Redirect",
|
||||||
|
"redirectSource": "Source",
|
||||||
|
"redirectAttachedTo": "Attached To",
|
||||||
|
"redirectDestination": "Destination",
|
||||||
|
"redirectType": "Type",
|
||||||
|
"redirectTypePermanent": "Permanent (301)",
|
||||||
|
"redirectTypeTemporary": "Temporary (302)",
|
||||||
|
"redirectUpdated": "Redirect updated successfully",
|
||||||
|
"redirectErrorUpdate": "Failed to update redirect",
|
||||||
|
"redirectDeleted": "Redirect deleted successfully",
|
||||||
|
"redirectErrorDelete": "Failed to delete redirect",
|
||||||
|
"redirectDelete": "Delete Redirect",
|
||||||
|
"redirectDeleteConfirm": "Confirm Delete Redirect",
|
||||||
|
"redirectQuestionRemove": "Are you sure you want to remove this redirect?",
|
||||||
|
"redirectMessageRemove": "Once removed, requests matching this redirect will no longer be forwarded."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { redirects, db } from "@server/db";
|
import { domains, redirects, resources, db } from "@server/db";
|
||||||
import type { Redirect } from "@server/db";
|
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { and, asc, eq, like, sql } from "drizzle-orm";
|
import { and, asc, eq, like, or, sql } from "drizzle-orm";
|
||||||
import type { PaginatedResponse } from "@server/types/Pagination";
|
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||||
|
|
||||||
export type ListRedirectsResponse = PaginatedResponse<{
|
export type ListRedirectsResponse = PaginatedResponse<{
|
||||||
redirects: Redirect[];
|
redirects: Array<{
|
||||||
|
redirectId: number;
|
||||||
|
orgId: string;
|
||||||
|
niceId: string;
|
||||||
|
name: string;
|
||||||
|
sourcePath: string;
|
||||||
|
destinationUrl: string | null;
|
||||||
|
permanent: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
resourceId: number | null;
|
||||||
|
resourceName: string | null;
|
||||||
|
resourceNiceId: string | null;
|
||||||
|
resourceFullDomain: string | null;
|
||||||
|
domainId: string | null;
|
||||||
|
baseDomain: string | null;
|
||||||
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
@@ -104,17 +118,36 @@ export async function listRedirects(
|
|||||||
const conditions = [eq(redirects.orgId, orgId)];
|
const conditions = [eq(redirects.orgId, orgId)];
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
|
const term = "%" + query.toLowerCase() + "%";
|
||||||
conditions.push(
|
conditions.push(
|
||||||
like(
|
or(
|
||||||
sql`LOWER(${redirects.name})`,
|
like(sql`LOWER(${redirects.name})`, term),
|
||||||
"%" + query.toLowerCase() + "%"
|
like(sql`LOWER(${redirects.sourcePath})`, term),
|
||||||
)
|
like(sql`LOWER(${redirects.destinationUrl})`, term)
|
||||||
|
)!
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseQuery = db
|
const baseQuery = db
|
||||||
.select()
|
.select({
|
||||||
|
redirectId: redirects.redirectId,
|
||||||
|
orgId: redirects.orgId,
|
||||||
|
niceId: redirects.niceId,
|
||||||
|
name: redirects.name,
|
||||||
|
sourcePath: redirects.sourcePath,
|
||||||
|
destinationUrl: redirects.destinationUrl,
|
||||||
|
permanent: redirects.permanent,
|
||||||
|
enabled: redirects.enabled,
|
||||||
|
resourceId: redirects.resourceId,
|
||||||
|
resourceName: resources.name,
|
||||||
|
resourceNiceId: resources.niceId,
|
||||||
|
resourceFullDomain: resources.fullDomain,
|
||||||
|
domainId: redirects.domainId,
|
||||||
|
baseDomain: domains.baseDomain
|
||||||
|
})
|
||||||
.from(redirects)
|
.from(redirects)
|
||||||
|
.leftJoin(resources, eq(resources.resourceId, redirects.resourceId))
|
||||||
|
.leftJoin(domains, eq(domains.domainId, redirects.domainId))
|
||||||
.where(and(...conditions));
|
.where(and(...conditions));
|
||||||
|
|
||||||
const countQuery = db.$count(
|
const countQuery = db.$count(
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import RedirectsTable from "@app/components/RedirectsTable";
|
||||||
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { internal } from "@app/lib/api";
|
||||||
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
|
import type { ListRedirectsResponse } from "@server/routers/redirect";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
@@ -7,12 +13,65 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
type RedirectIndexPageProps = {
|
type RedirectIndexPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
|
searchParams: Promise<Record<string, string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function ApiKeysPage(props: RedirectIndexPageProps) {
|
export default async function RedirectIndexPage(props: RedirectIndexPageProps) {
|
||||||
const params = await props.params;
|
const { orgId } = await props.params;
|
||||||
|
const searchParams = new URLSearchParams(await props.searchParams);
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
return null;
|
let redirects: ListRedirectsResponse["redirects"] = [];
|
||||||
|
let pagination: ListRedirectsResponse["pagination"] = {
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await internal.get<AxiosResponse<ListRedirectsResponse>>(
|
||||||
|
`/org/${orgId}/redirects?${searchParams.toString()}`,
|
||||||
|
await authCookieHeader()
|
||||||
|
);
|
||||||
|
const responseData = res.data.data;
|
||||||
|
redirects = responseData.redirects;
|
||||||
|
pagination = responseData.pagination;
|
||||||
|
} catch {
|
||||||
|
// empty list on error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsSectionTitle
|
||||||
|
title={t("redirectsTitle")}
|
||||||
|
description={t("redirectsDescription")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RedirectsTable
|
||||||
|
orgId={orgId}
|
||||||
|
redirects={redirects.map((redirect) => ({
|
||||||
|
redirectId: redirect.redirectId,
|
||||||
|
niceId: redirect.niceId,
|
||||||
|
name: redirect.name,
|
||||||
|
sourcePath: redirect.sourcePath,
|
||||||
|
destinationUrl: redirect.destinationUrl,
|
||||||
|
permanent: redirect.permanent,
|
||||||
|
enabled: redirect.enabled,
|
||||||
|
resourceId: redirect.resourceId,
|
||||||
|
resourceName: redirect.resourceName,
|
||||||
|
resourceNiceId: redirect.resourceNiceId,
|
||||||
|
resourceFullDomain: redirect.resourceFullDomain,
|
||||||
|
domainId: redirect.domainId,
|
||||||
|
baseDomain: redirect.baseDomain
|
||||||
|
}))}
|
||||||
|
rowCount={pagination.total}
|
||||||
|
pagination={{
|
||||||
|
pageIndex: pagination.page - 1,
|
||||||
|
pageSize: pagination.pageSize
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,367 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@app/components/ui/dropdown-menu";
|
||||||
|
import { Switch } from "@app/components/ui/switch";
|
||||||
|
import {
|
||||||
|
ControlledDataTable,
|
||||||
|
type ExtendedColumnDef
|
||||||
|
} from "@app/components/ui/controlled-data-table";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
|
import { ArrowRight, MoreHorizontal } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useMemo, useState, useTransition } from "react";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
|
export type RedirectRow = {
|
||||||
|
redirectId: number;
|
||||||
|
niceId: string;
|
||||||
|
name: string;
|
||||||
|
sourcePath: string;
|
||||||
|
destinationUrl: string | null;
|
||||||
|
permanent: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
resourceId: number | null;
|
||||||
|
resourceName: string | null;
|
||||||
|
resourceNiceId: string | null;
|
||||||
|
resourceFullDomain: string | null;
|
||||||
|
domainId: string | null;
|
||||||
|
baseDomain: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RedirectsTableProps = {
|
||||||
|
redirects: RedirectRow[];
|
||||||
|
orgId: string;
|
||||||
|
pagination: PaginationState;
|
||||||
|
rowCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RedirectsTable({
|
||||||
|
redirects,
|
||||||
|
orgId,
|
||||||
|
pagination,
|
||||||
|
rowCount
|
||||||
|
}: RedirectsTableProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
|
const api = createApiClient(useEnvContext());
|
||||||
|
const {
|
||||||
|
navigate: filter,
|
||||||
|
isNavigating: isFiltering,
|
||||||
|
searchParams
|
||||||
|
} = useNavigationContext();
|
||||||
|
|
||||||
|
const [rows, setRows] = useState(redirects);
|
||||||
|
const [selected, setSelected] = useState<RedirectRow | null>(null);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
|
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRows(redirects);
|
||||||
|
}, [redirects]);
|
||||||
|
|
||||||
|
function refreshData() {
|
||||||
|
startTransition(() => {
|
||||||
|
try {
|
||||||
|
router.refresh();
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaginationChange = (newPage: PaginationState) => {
|
||||||
|
searchParams.set("page", (newPage.pageIndex + 1).toString());
|
||||||
|
searchParams.set("pageSize", newPage.pageSize.toString());
|
||||||
|
filter({ searchParams });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = useDebouncedCallback((query: string) => {
|
||||||
|
searchParams.set("query", query);
|
||||||
|
searchParams.delete("page");
|
||||||
|
filter({ searchParams });
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
async function toggleEnabled(row: RedirectRow, enabled: boolean) {
|
||||||
|
setRows((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.redirectId === row.redirectId ? { ...r, enabled } : r
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post(`/org/${orgId}/redirects/${row.redirectId}`, {
|
||||||
|
enabled
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("redirectUpdated")
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
setRows((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.redirectId === row.redirectId
|
||||||
|
? { ...r, enabled: row.enabled }
|
||||||
|
: r
|
||||||
|
)
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("redirectErrorUpdate"),
|
||||||
|
description: formatAxiosError(e, t("redirectErrorUpdate"))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRedirect(row: RedirectRow) {
|
||||||
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
await api.delete(`/org/${orgId}/redirects/${row.redirectId}`);
|
||||||
|
setRows((prev) =>
|
||||||
|
prev.filter((r) => r.redirectId !== row.redirectId)
|
||||||
|
);
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setSelected(null);
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("redirectDeleted")
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("redirectErrorDelete"),
|
||||||
|
description: formatAxiosError(e, t("redirectErrorDelete"))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useMemo<ExtendedColumnDef<RedirectRow>[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
enableHiding: false,
|
||||||
|
header: () => <span className="p-3">{t("name")}</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Link
|
||||||
|
href={`/${orgId}/settings/redirects/${row.original.niceId}`}
|
||||||
|
className="hover:underline"
|
||||||
|
>
|
||||||
|
{row.original.name}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "source",
|
||||||
|
friendlyName: t("redirectSource"),
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">{t("redirectSource")}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const redirect = row.original;
|
||||||
|
const host =
|
||||||
|
redirect.resourceFullDomain ?? redirect.baseDomain;
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-sm">
|
||||||
|
{redirect.sourcePath}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="font-mono text-sm">
|
||||||
|
{host}
|
||||||
|
{redirect.sourcePath}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "attachedTo",
|
||||||
|
friendlyName: t("redirectAttachedTo"),
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">{t("redirectAttachedTo")}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const redirect = row.original;
|
||||||
|
|
||||||
|
if (redirect.resourceId && redirect.resourceNiceId) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/${orgId}/settings/resources/${redirect.resourceNiceId}`}
|
||||||
|
className="hover:underline"
|
||||||
|
>
|
||||||
|
{redirect.resourceName}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirect.baseDomain) {
|
||||||
|
return <span>{redirect.baseDomain}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>-</span>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "destinationUrl",
|
||||||
|
friendlyName: t("redirectDestination"),
|
||||||
|
header: () => (
|
||||||
|
<span className="p-3">{t("redirectDestination")}</span>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="font-mono text-sm">
|
||||||
|
{row.original.destinationUrl ?? "-"}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "permanent",
|
||||||
|
friendlyName: t("redirectType"),
|
||||||
|
header: () => <span className="p-3">{t("redirectType")}</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{row.original.permanent
|
||||||
|
? t("redirectTypePermanent")
|
||||||
|
: t("redirectTypeTemporary")}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "enabled",
|
||||||
|
friendlyName: t("enabled"),
|
||||||
|
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Switch
|
||||||
|
checked={row.original.enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
toggleEnabled(row.original, checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
enableHiding: false,
|
||||||
|
header: () => <span className="p-3" />,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-2 justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">
|
||||||
|
{t("openMenu")}
|
||||||
|
</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={`/${orgId}/settings/redirects/${row.original.niceId}`}
|
||||||
|
>
|
||||||
|
{t("edit")}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setSelected(row.original);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-red-500">
|
||||||
|
{t("delete")}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Link
|
||||||
|
href={`/${orgId}/settings/redirects/${row.original.niceId}`}
|
||||||
|
>
|
||||||
|
<Button variant="outline">
|
||||||
|
{t("edit")}
|
||||||
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[orgId, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{selected && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={isDeleteModalOpen}
|
||||||
|
setOpen={(val) => {
|
||||||
|
setIsDeleteModalOpen(val);
|
||||||
|
if (!val) {
|
||||||
|
setSelected(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("redirectQuestionRemove")}</p>
|
||||||
|
<p>{t("redirectMessageRemove")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("redirectDeleteConfirm")}
|
||||||
|
onConfirm={async () => deleteRedirect(selected)}
|
||||||
|
string={selected.name}
|
||||||
|
title={t("redirectDelete")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ControlledDataTable
|
||||||
|
columns={columns}
|
||||||
|
rows={rows}
|
||||||
|
addButtonText={t("redirectAdd")}
|
||||||
|
onAdd={() =>
|
||||||
|
startNavigation(() =>
|
||||||
|
router.push(`/${orgId}/settings/redirects/create`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isNavigatingToAddPage={isNavigatingToAddPage}
|
||||||
|
tableId="redirects-table"
|
||||||
|
searchPlaceholder={t("redirectsSearch")}
|
||||||
|
pagination={pagination}
|
||||||
|
onPaginationChange={handlePaginationChange}
|
||||||
|
searchQuery={searchParams.get("query")?.toString()}
|
||||||
|
onSearch={handleSearchChange}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing || isFiltering}
|
||||||
|
rowCount={rowCount}
|
||||||
|
columnVisibility={{
|
||||||
|
attachedTo: false
|
||||||
|
}}
|
||||||
|
enableColumnVisibility
|
||||||
|
stickyLeftColumn="name"
|
||||||
|
stickyRightColumn="actions"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user