mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-20 20:06:39 +00:00
Merge pull request #1612 from Pallavikumarimdb/fix/UI-adjustment
UI Adjustments
This commit is contained in:
@@ -468,7 +468,10 @@
|
|||||||
"createdAt": "Created At",
|
"createdAt": "Created At",
|
||||||
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
||||||
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
||||||
"proxyEnableSSL": "Enable SSL (https)",
|
"proxyEnableSSL": "Enable SSL",
|
||||||
|
"proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTarget": "Configure Targets",
|
||||||
"targetErrorFetch": "Failed to fetch targets",
|
"targetErrorFetch": "Failed to fetch targets",
|
||||||
"targetErrorFetchDescription": "An error occurred while fetching targets",
|
"targetErrorFetchDescription": "An error occurred while fetching targets",
|
||||||
"siteErrorFetch": "Failed to fetch resource",
|
"siteErrorFetch": "Failed to fetch resource",
|
||||||
@@ -495,7 +498,7 @@
|
|||||||
"targetTlsSettings": "Secure Connection Configuration",
|
"targetTlsSettings": "Secure Connection Configuration",
|
||||||
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
|
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
|
||||||
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
|
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
|
||||||
"targetTlsSni": "TLS Server Name (SNI)",
|
"targetTlsSni": "TLS Server Name",
|
||||||
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
||||||
"targetTlsSubmit": "Save Settings",
|
"targetTlsSubmit": "Save Settings",
|
||||||
"targets": "Targets Configuration",
|
"targets": "Targets Configuration",
|
||||||
@@ -504,9 +507,21 @@
|
|||||||
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
||||||
"methodSelect": "Select method",
|
"methodSelect": "Select method",
|
||||||
"targetSubmit": "Add Target",
|
"targetSubmit": "Add Target",
|
||||||
"targetNoOne": "No targets. Add a target using the form.",
|
"targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.",
|
||||||
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
|
||||||
"targetsSubmit": "Save Targets",
|
"targetsSubmit": "Save Targets",
|
||||||
|
"addTarget": "Add Target",
|
||||||
|
"targetErrorInvalidIp": "Invalid IP address",
|
||||||
|
"targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname",
|
||||||
|
"targetErrorInvalidPort": "Invalid port",
|
||||||
|
"targetErrorInvalidPortDescription": "Please enter a valid port number",
|
||||||
|
"targetErrorNoSite": "No site selected",
|
||||||
|
"targetErrorNoSiteDescription": "Please select a site for the target",
|
||||||
|
"targetCreated": "Target created",
|
||||||
|
"targetCreatedDescription": "Target has been created successfully",
|
||||||
|
"targetErrorCreate": "Failed to create target",
|
||||||
|
"targetErrorCreateDescription": "An error occurred while creating the target",
|
||||||
|
"save": "Save",
|
||||||
"proxyAdditional": "Additional Proxy Settings",
|
"proxyAdditional": "Additional Proxy Settings",
|
||||||
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",
|
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",
|
||||||
"proxyCustomHeader": "Custom Host Header",
|
"proxyCustomHeader": "Custom Host Header",
|
||||||
@@ -1410,6 +1425,7 @@
|
|||||||
"externalProxyEnabled": "External Proxy Enabled",
|
"externalProxyEnabled": "External Proxy Enabled",
|
||||||
"addNewTarget": "Add New Target",
|
"addNewTarget": "Add New Target",
|
||||||
"targetsList": "Targets List",
|
"targetsList": "Targets List",
|
||||||
|
"advancedMode": "Advanced Mode",
|
||||||
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
||||||
"healthCheckHealthy": "Healthy",
|
"healthCheckHealthy": "Healthy",
|
||||||
"healthCheckUnhealthy": "Unhealthy",
|
"healthCheckUnhealthy": "Unhealthy",
|
||||||
@@ -1740,5 +1756,7 @@
|
|||||||
"resourceHeaderAuthSetupTitle": "Set Header Authentication",
|
"resourceHeaderAuthSetupTitle": "Set Header Authentication",
|
||||||
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Leave both fields blank to remove existing header authentication.",
|
"resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Leave both fields blank to remove existing header authentication.",
|
||||||
"resourceHeaderAuthSubmit": "Set Header Authentication",
|
"resourceHeaderAuthSubmit": "Set Header Authentication",
|
||||||
"actionSetResourceHeaderAuth": "Set Header Authentication"
|
"actionSetResourceHeaderAuth": "Set Header Authentication",
|
||||||
|
"priority": "Priority",
|
||||||
|
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ export async function moveEmailToAudience(
|
|||||||
email: string,
|
email: string,
|
||||||
audienceId: AudienceIds
|
audienceId: AudienceIds
|
||||||
) {
|
) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,8 @@ const createHttpResourceSchema = z
|
|||||||
subdomain: z.string().nullable().optional(),
|
subdomain: z.string().nullable().optional(),
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.enum(["tcp", "udp"]),
|
protocol: z.enum(["tcp", "udp"]),
|
||||||
domainId: z.string()
|
domainId: z.string(),
|
||||||
|
stickySession: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine(
|
.refine(
|
||||||
@@ -191,6 +192,7 @@ async function createHttpResource(
|
|||||||
|
|
||||||
const { name, domainId } = parsedBody.data;
|
const { name, domainId } = parsedBody.data;
|
||||||
const subdomain = parsedBody.data.subdomain;
|
const subdomain = parsedBody.data.subdomain;
|
||||||
|
const stickySession=parsedBody.data.stickySession;
|
||||||
|
|
||||||
// Validate domain and construct full domain
|
// Validate domain and construct full domain
|
||||||
const domainResult = await validateAndConstructDomain(
|
const domainResult = await validateAndConstructDomain(
|
||||||
@@ -254,7 +256,8 @@ async function createHttpResource(
|
|||||||
subdomain: finalSubdomain,
|
subdomain: finalSubdomain,
|
||||||
http: true,
|
http: true,
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
ssl: true
|
ssl: true,
|
||||||
|
stickySession: stickySession
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,15 @@ import { useTranslations } from "next-intl";
|
|||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IdpDataTable<TData, TValue>({
|
export function IdpDataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data
|
data,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -29,6 +33,8 @@ export function IdpDataTable<TData, TValue>({
|
|||||||
onAdd={() => {
|
onAdd={() => {
|
||||||
router.push("/admin/idp/create");
|
router.push("/admin/idp/create");
|
||||||
}}
|
}}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,26 @@ export default function IdpTable({ idps }: Props) {
|
|||||||
const [selectedIdp, setSelectedIdp] = useState<IdpRow | null>(null);
|
const [selectedIdp, setSelectedIdp] = useState<IdpRow | null>(null);
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteIdp = async (idpId: number) => {
|
const deleteIdp = async (idpId: number) => {
|
||||||
try {
|
try {
|
||||||
await api.delete(`/idp/${idpId}`);
|
await api.delete(`/idp/${idpId}`);
|
||||||
@@ -194,7 +212,12 @@ export default function IdpTable({ idps }: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<IdpDataTable columns={columns} data={idps} />
|
<IdpDataTable
|
||||||
|
columns={columns}
|
||||||
|
data={idps}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ import { useTranslations } from "next-intl";
|
|||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsersDataTable<TData, TValue>({
|
export function UsersDataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data
|
data,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -26,6 +30,8 @@ export function UsersDataTable<TData, TValue>({
|
|||||||
title={t('userServer')}
|
title={t('userServer')}
|
||||||
searchPlaceholder={t('userSearch')}
|
searchPlaceholder={t('userSearch')}
|
||||||
searchColumn="email"
|
searchColumn="email"
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,25 @@ export default function UsersTable({ users }: Props) {
|
|||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteUser = (id: string) => {
|
const deleteUser = (id: string) => {
|
||||||
api.delete(`/user/${id}`)
|
api.delete(`/user/${id}`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -168,7 +187,7 @@ export default function UsersTable({ users }: Props) {
|
|||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<span>
|
<span>
|
||||||
{userRow.twoFactorEnabled ||
|
{userRow.twoFactorEnabled ||
|
||||||
userRow.twoFactorSetupRequested ? (
|
userRow.twoFactorSetupRequested ? (
|
||||||
<span className="text-green-500">
|
<span className="text-green-500">
|
||||||
{t("enabled")}
|
{t("enabled")}
|
||||||
</span>
|
</span>
|
||||||
@@ -263,7 +282,12 @@ export default function UsersTable({ users }: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<UsersDataTable columns={columns} data={rows} />
|
<UsersDataTable
|
||||||
|
columns={columns}
|
||||||
|
data={rows}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,16 +33,20 @@ interface DataTableProps<TData, TValue> {
|
|||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
addApiKey?: () => void;
|
addApiKey?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ApiKeysDataTable<TData, TValue>({
|
export function ApiKeysDataTable<TData, TValue>({
|
||||||
addApiKey,
|
addApiKey,
|
||||||
columns,
|
columns,
|
||||||
data
|
data,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -53,6 +57,8 @@ export function ApiKeysDataTable<TData, TValue>({
|
|||||||
searchColumn="name"
|
searchColumn="name"
|
||||||
onAdd={addApiKey}
|
onAdd={addApiKey}
|
||||||
addButtonText={t('apiKeysAdd')}
|
addButtonText={t('apiKeysAdd')}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,25 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
|||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteSite = (apiKeyId: string) => {
|
const deleteSite = (apiKeyId: string) => {
|
||||||
api.delete(`/api-key/${apiKeyId}`)
|
api.delete(`/api-key/${apiKeyId}`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -186,6 +205,8 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
|||||||
addApiKey={() => {
|
addApiKey={() => {
|
||||||
router.push(`/admin/api-keys/create`);
|
router.push(`/admin/api-keys/create`);
|
||||||
}}
|
}}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ import { DataTable } from "@app/components/ui/data-table";
|
|||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
addClient?: () => void;
|
addClient?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClientsDataTable<TData, TValue>({
|
export function ClientsDataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
addClient
|
addClient,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
@@ -25,6 +29,8 @@ export function ClientsDataTable<TData, TValue>({
|
|||||||
searchPlaceholder="Search clients..."
|
searchPlaceholder="Search clients..."
|
||||||
searchColumn="name"
|
searchColumn="name"
|
||||||
onAdd={addClient}
|
onAdd={addClient}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
addButtonText="Add Client"
|
addButtonText="Add Client"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { toast } from "@app/hooks/useToast";
|
|||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export type ClientRow = {
|
export type ClientRow = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -53,6 +54,25 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
|||||||
const [rows, setRows] = useState<ClientRow[]>(clients);
|
const [rows, setRows] = useState<ClientRow[]>(clients);
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteClient = (clientId: number) => {
|
const deleteClient = (clientId: number) => {
|
||||||
api.delete(`/client/${clientId}`)
|
api.delete(`/client/${clientId}`)
|
||||||
@@ -207,32 +227,32 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
{/* <Link */}
|
{/* <Link */}
|
||||||
{/* className="block w-full" */}
|
{/* className="block w-full" */}
|
||||||
{/* href={`/${clientRow.orgId}/settings/sites/${clientRow.nice}`} */}
|
{/* href={`/${clientRow.orgId}/settings/sites/${clientRow.nice}`} */}
|
||||||
{/* > */}
|
{/* > */}
|
||||||
{/* <DropdownMenuItem> */}
|
{/* <DropdownMenuItem> */}
|
||||||
{/* View settings */}
|
{/* View settings */}
|
||||||
{/* </DropdownMenuItem> */}
|
{/* </DropdownMenuItem> */}
|
||||||
{/* </Link> */}
|
{/* </Link> */}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedClient(clientRow);
|
setSelectedClient(clientRow);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-red-500">Delete</span>
|
<span className="text-red-500">Delete</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Link
|
<Link
|
||||||
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
||||||
>
|
>
|
||||||
@@ -292,6 +312,8 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
|||||||
addClient={() => {
|
addClient={() => {
|
||||||
router.push(`/${orgId}/settings/clients/create`);
|
router.push(`/${orgId}/settings/clients/create`);
|
||||||
}}
|
}}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function HealthCheckDialog({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
// Determine default scheme from target method
|
// Determine default scheme from target method
|
||||||
const getDefaultScheme = () => {
|
const getDefaultScheme = () => {
|
||||||
if (initialConfig?.hcScheme) {
|
if (initialConfig?.hcScheme) {
|
||||||
@@ -177,7 +177,7 @@ export default function HealthCheckDialog({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("enableHealthChecks")}
|
{t("enableHealthChecks")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -210,7 +210,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcScheme"
|
name="hcScheme"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("healthScheme")}
|
{t("healthScheme")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -250,7 +250,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcHostname"
|
name="hcHostname"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("healthHostname")}
|
{t("healthHostname")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -277,7 +277,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcPort"
|
name="hcPort"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("healthPort")}
|
{t("healthPort")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -308,7 +308,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcPath"
|
name="hcPath"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("healthCheckPath")}
|
{t("healthCheckPath")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -338,7 +338,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcMethod"
|
name="hcMethod"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("httpMethod")}
|
{t("httpMethod")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -390,7 +390,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcInterval"
|
name="hcInterval"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t(
|
{t(
|
||||||
"healthyIntervalSeconds"
|
"healthyIntervalSeconds"
|
||||||
)}
|
)}
|
||||||
@@ -425,7 +425,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcUnhealthyInterval"
|
name="hcUnhealthyInterval"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t(
|
{t(
|
||||||
"unhealthyIntervalSeconds"
|
"unhealthyIntervalSeconds"
|
||||||
)}
|
)}
|
||||||
@@ -460,7 +460,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcTimeout"
|
name="hcTimeout"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("timeoutSeconds")}
|
{t("timeoutSeconds")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -499,7 +499,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcStatus"
|
name="hcStatus"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("expectedResponseCodes")}
|
{t("expectedResponseCodes")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -541,7 +541,7 @@ export default function HealthCheckDialog({
|
|||||||
name="hcHeaders"
|
name="hcHeaders"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-base font-semibold">
|
<FormLabel>
|
||||||
{t("customHeaders")}
|
{t("customHeaders")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ import { useTranslations } from 'next-intl';
|
|||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InvitationsDataTable<TData, TValue>({
|
export function InvitationsDataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data
|
data,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -26,6 +30,8 @@ export function InvitationsDataTable<TData, TValue>({
|
|||||||
title={t('invite')}
|
title={t('invite')}
|
||||||
searchPlaceholder={t('inviteSearch')}
|
searchPlaceholder={t('inviteSearch')}
|
||||||
searchColumn="email"
|
searchColumn="email"
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { createApiClient } from "@app/lib/api";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export type InvitationRow = {
|
export type InvitationRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -45,6 +46,25 @@ export default function InvitationsTable({
|
|||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
const router = useRouter();
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<InvitationRow>[] = [
|
const columns: ColumnDef<InvitationRow>[] = [
|
||||||
{
|
{
|
||||||
@@ -185,7 +205,12 @@ export default function InvitationsTable({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InvitationsDataTable columns={columns} data={invitations} />
|
<InvitationsDataTable
|
||||||
|
columns={columns}
|
||||||
|
data={invitations}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ interface DataTableProps<TData, TValue> {
|
|||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
addApiKey?: () => void;
|
addApiKey?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OrgApiKeysDataTable<TData, TValue>({
|
export function OrgApiKeysDataTable<TData, TValue>({
|
||||||
addApiKey,
|
addApiKey,
|
||||||
columns,
|
columns,
|
||||||
data
|
data,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -27,6 +31,8 @@ export function OrgApiKeysDataTable<TData, TValue>({
|
|||||||
searchPlaceholder={t('searchApiKeys')}
|
searchPlaceholder={t('searchApiKeys')}
|
||||||
searchColumn="name"
|
searchColumn="name"
|
||||||
onAdd={addApiKey}
|
onAdd={addApiKey}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
addButtonText={t('apiKeysAdd')}
|
addButtonText={t('apiKeysAdd')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,24 @@ export default function OrgApiKeysTable({
|
|||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteSite = (apiKeyId: string) => {
|
const deleteSite = (apiKeyId: string) => {
|
||||||
api.delete(`/org/${orgId}/api-key/${apiKeyId}`)
|
api.delete(`/org/${orgId}/api-key/${apiKeyId}`)
|
||||||
@@ -195,6 +213,8 @@ export default function OrgApiKeysTable({
|
|||||||
addApiKey={() => {
|
addApiKey={() => {
|
||||||
router.push(`/${orgId}/settings/api-keys/create`);
|
router.push(`/${orgId}/settings/api-keys/create`);
|
||||||
}}
|
}}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Pencil } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -6,20 +6,13 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@app/components/ui/dialog";
|
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { Label } from "@app/components/ui/label";
|
import { Label } from "@app/components/ui/label";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import { Credenza, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle, CredenzaTrigger } from "./Credenza";
|
||||||
|
|
||||||
|
|
||||||
export function PathMatchModal({
|
export function PathMatchModal({
|
||||||
@@ -68,15 +61,15 @@ export function PathMatchModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Credenza open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
<CredenzaTrigger asChild>{trigger}</CredenzaTrigger>
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
<CredenzaContent className="sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<CredenzaHeader>
|
||||||
<DialogTitle>Configure Path Matching</DialogTitle>
|
<CredenzaTitle>Configure Path Matching</CredenzaTitle>
|
||||||
<DialogDescription>
|
<CredenzaDescription>
|
||||||
Set up how incoming requests should be matched based on their path.
|
Set up how incoming requests should be matched based on their path.
|
||||||
</DialogDescription>
|
</CredenzaDescription>
|
||||||
</DialogHeader>
|
</CredenzaHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="match-type">Match Type</Label>
|
<Label htmlFor="match-type">Match Type</Label>
|
||||||
@@ -102,7 +95,7 @@ export function PathMatchModal({
|
|||||||
<p className="text-sm text-muted-foreground">{getHelpText()}</p>
|
<p className="text-sm text-muted-foreground">{getHelpText()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="gap-2">
|
<CredenzaFooter className="gap-2">
|
||||||
{value?.path && (
|
{value?.path && (
|
||||||
<Button variant="outline" onClick={handleClear}>
|
<Button variant="outline" onClick={handleClear}>
|
||||||
Clear
|
Clear
|
||||||
@@ -111,9 +104,9 @@ export function PathMatchModal({
|
|||||||
<Button onClick={handleSave} disabled={!path.trim()}>
|
<Button onClick={handleSave} disabled={!path.trim()}>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</CredenzaFooter>
|
||||||
</DialogContent>
|
</CredenzaContent>
|
||||||
</Dialog>
|
</Credenza>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,17 +170,17 @@ export function PathRewriteModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Credenza open={open} onOpenChange={(v) => !disabled && setOpen(v)}>
|
||||||
<DialogTrigger asChild disabled={disabled}>
|
<CredenzaTrigger asChild>
|
||||||
{trigger}
|
{trigger}
|
||||||
</DialogTrigger>
|
</CredenzaTrigger>
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
<CredenzaContent className="sm:max-w-[500px]">
|
||||||
<DialogHeader>
|
<CredenzaHeader>
|
||||||
<DialogTitle>Configure Path Rewriting</DialogTitle>
|
<CredenzaTitle>Configure Path Rewriting</CredenzaTitle>
|
||||||
<DialogDescription>
|
<CredenzaDescription>
|
||||||
Transform the matched path before forwarding to the target.
|
Transform the matched path before forwarding to the target.
|
||||||
</DialogDescription>
|
</CredenzaDescription>
|
||||||
</DialogHeader>
|
</CredenzaHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="rewrite-type">Rewrite Type</Label>
|
<Label htmlFor="rewrite-type">Rewrite Type</Label>
|
||||||
@@ -214,7 +207,7 @@ export function PathRewriteModal({
|
|||||||
<p className="text-sm text-muted-foreground">{getHelpText()}</p>
|
<p className="text-sm text-muted-foreground">{getHelpText()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="gap-2">
|
<CredenzaFooter className="gap-2">
|
||||||
{value?.rewritePath && (
|
{value?.rewritePath && (
|
||||||
<Button variant="outline" onClick={handleClear}>
|
<Button variant="outline" onClick={handleClear}>
|
||||||
Clear
|
Clear
|
||||||
@@ -226,9 +219,9 @@ export function PathRewriteModal({
|
|||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</CredenzaFooter>
|
||||||
</DialogContent>
|
</CredenzaContent>
|
||||||
</Dialog>
|
</Credenza>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,13 +243,13 @@ export function PathMatchDisplay({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 w-full text-left">
|
<div className="flex items-center gap-2 w-full text-left">
|
||||||
<Badge variant="secondary" className="font-mono text-xs shrink-0">
|
<Badge variant="secondary" className="text-xs shrink-0">
|
||||||
{getTypeLabel(value.pathMatchType)}
|
{getTypeLabel(value.pathMatchType)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<code className="text-sm flex-1 truncate" title={value.path}>
|
<code className="text-sm flex-1 truncate" title={value.path}>
|
||||||
{value.path}
|
{value.path}
|
||||||
</code>
|
</code>
|
||||||
<Pencil className="h-3 w-3 shrink-0 opacity-70" />
|
<Settings className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -281,13 +274,13 @@ export function PathRewriteDisplay({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 w-full text-left">
|
<div className="flex items-center gap-2 w-full text-left">
|
||||||
<Badge variant="secondary" className="font-mono text-xs shrink-0">
|
<Badge variant="secondary" className="text-xs shrink-0">
|
||||||
{getTypeLabel(value.rewritePathType)}
|
{getTypeLabel(value.rewritePathType)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<code className="text-sm flex-1 truncate" title={value.rewritePath || ""}>
|
<code className="text-sm flex-1 truncate" title={value.rewritePath || ""}>
|
||||||
{value.rewritePath || <span className="text-muted-foreground italic">(strip)</span>}
|
{value.rewritePath || <span className="text-muted-foreground italic">(strip)</span>}
|
||||||
</code>
|
</code>
|
||||||
<Pencil className="h-3 w-3 shrink-0 opacity-70" />
|
<Settings className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import {
|
|||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
ShieldOff,
|
ShieldOff,
|
||||||
ShieldCheck
|
ShieldCheck,
|
||||||
|
RefreshCw
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -179,9 +180,27 @@ export default function ResourcesTable({
|
|||||||
const [internalColumnFilters, setInternalColumnFilters] =
|
const [internalColumnFilters, setInternalColumnFilters] =
|
||||||
useState<ColumnFiltersState>([]);
|
useState<ColumnFiltersState>([]);
|
||||||
const [internalGlobalFilter, setInternalGlobalFilter] = useState<any>([]);
|
const [internalGlobalFilter, setInternalGlobalFilter] = useState<any>([]);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
const currentView = searchParams.get("view") || defaultView;
|
const currentView = searchParams.get("view") || defaultView;
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSites = async () => {
|
const fetchSites = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -753,7 +772,21 @@ export default function ResourcesTable({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 sm:justify-end">
|
<div className="flex items-center gap-2 sm:justify-end">
|
||||||
{getActionButton()}
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={refreshData}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
{t("refresh")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{getActionButton()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ interface DataTableProps<TData, TValue> {
|
|||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
createRole?: () => void;
|
createRole?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RolesDataTable<TData, TValue>({
|
export function RolesDataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
createRole
|
createRole,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -29,6 +33,8 @@ export function RolesDataTable<TData, TValue>({
|
|||||||
searchPlaceholder={t('accessRolesSearch')}
|
searchPlaceholder={t('accessRolesSearch')}
|
||||||
searchColumn="name"
|
searchColumn="name"
|
||||||
onAdd={createRole}
|
onAdd={createRole}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
addButtonText={t('accessRolesAdd')}
|
addButtonText={t('accessRolesAdd')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import DeleteRoleForm from "@app/components/DeleteRoleForm";
|
|||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export type RoleRow = Role;
|
export type RoleRow = Role;
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ type RolesTableProps = {
|
|||||||
export default function UsersTable({ roles: r }: RolesTableProps) {
|
export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [roles, setRoles] = useState<RoleRow[]>(r);
|
const [roles, setRoles] = useState<RoleRow[]>(r);
|
||||||
|
|
||||||
@@ -40,6 +42,24 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<RoleRow>[] = [
|
const columns: ColumnDef<RoleRow>[] = [
|
||||||
{
|
{
|
||||||
@@ -116,6 +136,8 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||||||
createRole={() => {
|
createRole={() => {
|
||||||
setIsCreateModalOpen(true);
|
setIsCreateModalOpen(true);
|
||||||
}}
|
}}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ interface DataTableProps<TData, TValue> {
|
|||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
createShareLink?: () => void;
|
createShareLink?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShareLinksDataTable<TData, TValue>({
|
export function ShareLinksDataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
createShareLink
|
createShareLink,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -29,6 +33,8 @@ export function ShareLinksDataTable<TData, TValue>({
|
|||||||
searchPlaceholder={t('shareSearch')}
|
searchPlaceholder={t('shareSearch')}
|
||||||
searchColumn="name"
|
searchColumn="name"
|
||||||
onAdd={createShareLink}
|
onAdd={createShareLink}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
addButtonText={t('shareCreate')}
|
addButtonText={t('shareCreate')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,6 +61,25 @@ export default function ShareLinksTable({
|
|||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const [rows, setRows] = useState<ShareLinkRow[]>(shareLinks);
|
const [rows, setRows] = useState<ShareLinkRow[]>(shareLinks);
|
||||||
|
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function formatLink(link: string) {
|
function formatLink(link: string) {
|
||||||
return link.substring(0, 20) + "..." + link.substring(link.length - 20);
|
return link.substring(0, 20) + "..." + link.substring(link.length - 20);
|
||||||
}
|
}
|
||||||
@@ -292,6 +311,8 @@ export default function ShareLinksTable({
|
|||||||
createShareLink={() => {
|
createShareLink={() => {
|
||||||
setIsCreateModalOpen(true);
|
setIsCreateModalOpen(true);
|
||||||
}}
|
}}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ interface DataTableProps<TData, TValue> {
|
|||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
inviteUser?: () => void;
|
inviteUser?: () => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
isRefreshing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsersDataTable<TData, TValue>({
|
export function UsersDataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
inviteUser
|
inviteUser,
|
||||||
|
onRefresh,
|
||||||
|
isRefreshing
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -29,6 +33,8 @@ export function UsersDataTable<TData, TValue>({
|
|||||||
searchPlaceholder={t('accessUsersSearch')}
|
searchPlaceholder={t('accessUsersSearch')}
|
||||||
searchColumn="email"
|
searchColumn="email"
|
||||||
onAdd={inviteUser}
|
onAdd={inviteUser}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
addButtonText={t('accessUserCreate')}
|
addButtonText={t('accessUserCreate')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,6 +51,24 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
const { user, updateUser } = useUserContext();
|
const { user, updateUser } = useUserContext();
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<UserRow>[] = [
|
const columns: ColumnDef<UserRow>[] = [
|
||||||
{
|
{
|
||||||
@@ -290,6 +308,8 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||||||
`/${org?.org.orgId}/settings/access/users/create`
|
`/${org?.org.orgId}/settings/access/users/create`
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
onRefresh={refreshData}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[80px] w-full rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex min-h-[80px] w-full rounded-md border border-input bg-card px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-[color,box-shadow]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
Reference in New Issue
Block a user