improve spacing and column layout on mobile
This commit is contained in:
parent
152a62a27b
commit
32ffb33d98
20 changed files with 400 additions and 346 deletions
|
@ -8,8 +8,9 @@ export function createApiClient({ env }: { env: env }): AxiosInstance {
|
||||||
return apiInstance;
|
return apiInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiInstance) {
|
if (typeof window === "undefined") {
|
||||||
return apiInstance
|
// @ts-ignore
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let baseURL;
|
let baseURL;
|
||||||
|
@ -45,7 +46,8 @@ export const internal = axios.create({
|
||||||
baseURL: `http://localhost:${process.env.SERVER_EXTERNAL_PORT}/api/v1`,
|
baseURL: `http://localhost:${process.env.SERVER_EXTERNAL_PORT}/api/v1`,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRF-Token": "x-csrf-protection"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -40,26 +40,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const columns: ColumnDef<RoleRow>[] = [
|
const columns: ColumnDef<RoleRow>[] = [
|
||||||
{
|
|
||||||
accessorKey: "name",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "description",
|
|
||||||
header: "Description"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
@ -67,14 +47,9 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-end">
|
<div>
|
||||||
{roleRow.isAdmin && (
|
{roleRow.isAdmin && (
|
||||||
<Button
|
<MoreHorizontal className="h-4 w-4 opacity-0" />
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 p-0 opacity-0 cursor-default"
|
|
||||||
>
|
|
||||||
Placeholder
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
{!roleRow.isAdmin && (
|
{!roleRow.isAdmin && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
@ -107,6 +82,26 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "description",
|
||||||
|
header: "Description"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,64 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const columns: ColumnDef<UserRow>[] = [
|
const columns: ColumnDef<UserRow>[] = [
|
||||||
|
{
|
||||||
|
id: "dots",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const userRow = row.original;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
{userRow.isOwner && (
|
||||||
|
<MoreHorizontal className="h-4 w-4 opacity-0" />
|
||||||
|
)}
|
||||||
|
{!userRow.isOwner && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">
|
||||||
|
Open menu
|
||||||
|
</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link
|
||||||
|
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||||
|
className="block w-full"
|
||||||
|
>
|
||||||
|
Manage User
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{userRow.email !== user?.email && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteModalOpen(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
setSelectedUser(
|
||||||
|
userRow
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-red-500">
|
||||||
|
Remove User
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "email",
|
accessorKey: "email",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -114,73 +172,27 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const userRow = row.original;
|
const userRow = row.original;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex items-center justify-end">
|
||||||
<div className="flex items-center justify-end">
|
{userRow.isOwner && (
|
||||||
{userRow.isOwner && (
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
className="opacity-0 cursor-default"
|
||||||
className="opacity-0 cursor-default"
|
>
|
||||||
>
|
Placeholder
|
||||||
Placeholder
|
</Button>
|
||||||
|
)}
|
||||||
|
{!userRow.isOwner && (
|
||||||
|
<Link
|
||||||
|
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||||
|
>
|
||||||
|
<Button variant={"gray"} className="ml-2">
|
||||||
|
Manage
|
||||||
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</Link>
|
||||||
{!userRow.isOwner && (
|
)}
|
||||||
<>
|
</div>
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
<span className="sr-only">
|
|
||||||
Open menu
|
|
||||||
</span>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Link
|
|
||||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
|
||||||
>
|
|
||||||
Manage User
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{userRow.email !== user?.email && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setIsDeleteModalOpen(
|
|
||||||
true
|
|
||||||
);
|
|
||||||
setSelectedUser(
|
|
||||||
userRow
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-red-500">
|
|
||||||
Remove User
|
|
||||||
</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Link
|
|
||||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant={"gray"}
|
|
||||||
className="ml-2"
|
|
||||||
>
|
|
||||||
Manage
|
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md">
|
<div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md lg:max-w-xl">
|
||||||
<LinkIcon className="h-4 w-4" />
|
<LinkIcon className="h-4 w-4" />
|
||||||
<a
|
<a
|
||||||
href={fullUrl}
|
href={fullUrl}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
|
@ -24,7 +24,7 @@ import {
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { CreateTargetResponse } from "@server/routers/target";
|
import { CreateTargetResponse } from "@server/routers/target";
|
||||||
import {
|
import {
|
||||||
|
@ -34,7 +34,7 @@ import {
|
||||||
getPaginationRowModel,
|
getPaginationRowModel,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
flexRender,
|
flexRender
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
|
@ -42,7 +42,7 @@ import {
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow
|
||||||
} from "@app/components/ui/table";
|
} from "@app/components/ui/table";
|
||||||
import { useToast } from "@app/hooks/useToast";
|
import { useToast } from "@app/hooks/useToast";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
@ -59,9 +59,9 @@ const addTargetSchema = z.object({
|
||||||
port: z
|
port: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => !isNaN(Number(val)), {
|
.refine((val) => !isNaN(Number(val)), {
|
||||||
message: "Port must be a number",
|
message: "Port must be a number"
|
||||||
})
|
})
|
||||||
.transform((val) => Number(val)),
|
.transform((val) => Number(val))
|
||||||
// protocol: z.string(),
|
// protocol: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,16 +99,16 @@ export default function ReverseProxyTargets(props: {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: "",
|
ip: "",
|
||||||
method: "http",
|
method: "http",
|
||||||
port: "80",
|
port: "80"
|
||||||
// protocol: "TCP",
|
// protocol: "TCP",
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTargets = async () => {
|
const fetchTargets = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
|
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
|
||||||
`/resource/${params.resourceId}/targets`,
|
`/resource/${params.resourceId}/targets`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
|
@ -121,8 +121,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
title: "Failed to fetch targets",
|
title: "Failed to fetch targets",
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
err,
|
err,
|
||||||
"An error occurred while fetching targets",
|
"An error occurred while fetching targets"
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setPageLoading(false);
|
setPageLoading(false);
|
||||||
|
@ -133,7 +133,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
const fetchSite = async () => {
|
const fetchSite = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get<AxiosResponse<GetSiteResponse>>(
|
const res = await api.get<AxiosResponse<GetSiteResponse>>(
|
||||||
`/site/${resource.siteId}`,
|
`/site/${resource.siteId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
|
@ -146,27 +146,28 @@ export default function ReverseProxyTargets(props: {
|
||||||
title: "Failed to fetch resource",
|
title: "Failed to fetch resource",
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
err,
|
err,
|
||||||
"An error occurred while fetching resource",
|
"An error occurred while fetching resource"
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
fetchSite();
|
fetchSite();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function addTarget(data: AddTargetFormValues) {
|
async function addTarget(data: AddTargetFormValues) {
|
||||||
// Check if target with same IP, port and method already exists
|
// Check if target with same IP, port and method already exists
|
||||||
const isDuplicate = targets.some(
|
const isDuplicate = targets.some(
|
||||||
target => target.ip === data.ip &&
|
(target) =>
|
||||||
target.port === data.port &&
|
target.ip === data.ip &&
|
||||||
target.method === data.method
|
target.port === data.port &&
|
||||||
|
target.method === data.method
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Duplicate target",
|
title: "Duplicate target",
|
||||||
description: "A target with these settings already exists",
|
description: "A target with these settings already exists"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -179,7 +180,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Invalid target IP",
|
title: "Invalid target IP",
|
||||||
description: "Target IP must be within the site subnet",
|
description: "Target IP must be within the site subnet"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -190,7 +191,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
targetId: new Date().getTime(),
|
targetId: new Date().getTime(),
|
||||||
new: true,
|
new: true,
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId
|
||||||
};
|
};
|
||||||
|
|
||||||
setTargets([...targets, newTarget]);
|
setTargets([...targets, newTarget]);
|
||||||
|
@ -199,7 +200,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
|
|
||||||
const removeTarget = (targetId: number) => {
|
const removeTarget = (targetId: number) => {
|
||||||
setTargets([
|
setTargets([
|
||||||
...targets.filter((target) => target.targetId !== targetId),
|
...targets.filter((target) => target.targetId !== targetId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!targets.find((target) => target.targetId === targetId)?.new) {
|
if (!targets.find((target) => target.targetId === targetId)?.new) {
|
||||||
|
@ -212,8 +213,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? { ...target, ...data, updated: true }
|
? { ...target, ...data, updated: true }
|
||||||
: target,
|
: target
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +223,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const res = await api.post(`/resource/${params.resourceId}`, {
|
const res = await api.post(`/resource/${params.resourceId}`, {
|
||||||
ssl: sslEnabled,
|
ssl: sslEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
updateResource({ ssl: sslEnabled });
|
updateResource({ ssl: sslEnabled });
|
||||||
|
@ -233,7 +234,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
port: target.port,
|
port: target.port,
|
||||||
// protocol: target.protocol,
|
// protocol: target.protocol,
|
||||||
method: target.method,
|
method: target.method,
|
||||||
enabled: target.enabled,
|
enabled: target.enabled
|
||||||
};
|
};
|
||||||
|
|
||||||
if (target.new) {
|
if (target.new) {
|
||||||
|
@ -244,7 +245,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
} else if (target.updated) {
|
} else if (target.updated) {
|
||||||
const res = await api.post(
|
const res = await api.post(
|
||||||
`/target/${target.targetId}`,
|
`/target/${target.targetId}`,
|
||||||
data,
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,23 +254,23 @@ export default function ReverseProxyTargets(props: {
|
||||||
let res = {
|
let res = {
|
||||||
...t,
|
...t,
|
||||||
new: false,
|
new: false,
|
||||||
updated: false,
|
updated: false
|
||||||
};
|
};
|
||||||
return res;
|
return res;
|
||||||
}),
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const targetId of targetsToRemove) {
|
for (const targetId of targetsToRemove) {
|
||||||
await api.delete(`/target/${targetId}`);
|
await api.delete(`/target/${targetId}`);
|
||||||
setTargets(
|
setTargets(
|
||||||
targets.filter((target) => target.targetId !== targetId),
|
targets.filter((target) => target.targetId !== targetId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Resource updated",
|
title: "Resource updated",
|
||||||
description: "Resource and targets updated successfully",
|
description: "Resource and targets updated successfully"
|
||||||
});
|
});
|
||||||
|
|
||||||
setTargetsToRemove([]);
|
setTargetsToRemove([]);
|
||||||
|
@ -280,8 +281,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
title: "Operation failed",
|
title: "Operation failed",
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
err,
|
err,
|
||||||
"An error occurred during the save operation",
|
"An error occurred during the save operation"
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,13 +300,15 @@ export default function ReverseProxyTargets(props: {
|
||||||
updateTarget(row.original.targetId, { method: value })
|
updateTarget(row.original.targetId, { method: value })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>{row.original.method}</SelectTrigger>
|
<SelectTrigger className="min-w-[100px]">
|
||||||
|
{row.original.method}
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="http">http</SelectItem>
|
<SelectItem value="http">http</SelectItem>
|
||||||
<SelectItem value="https">https</SelectItem>
|
<SelectItem value="https">https</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
),
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "ip",
|
||||||
|
@ -313,13 +316,14 @@ export default function ReverseProxyTargets(props: {
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Input
|
<Input
|
||||||
defaultValue={row.original.ip}
|
defaultValue={row.original.ip}
|
||||||
|
className="min-w-[150px]"
|
||||||
onBlur={(e) =>
|
onBlur={(e) =>
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
ip: e.target.value,
|
ip: e.target.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "port",
|
accessorKey: "port",
|
||||||
|
@ -328,13 +332,14 @@ export default function ReverseProxyTargets(props: {
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
defaultValue={row.original.port}
|
defaultValue={row.original.port}
|
||||||
|
className="min-w-[100px]"
|
||||||
onBlur={(e) =>
|
onBlur={(e) =>
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
port: parseInt(e.target.value, 10),
|
port: parseInt(e.target.value, 10)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// accessorKey: "protocol",
|
// accessorKey: "protocol",
|
||||||
|
@ -364,7 +369,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
updateTarget(row.original.targetId, { enabled: val })
|
updateTarget(row.original.targetId, { enabled: val })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
|
@ -387,8 +392,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
)
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
|
@ -397,7 +402,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pageLoading) {
|
if (pageLoading) {
|
||||||
|
@ -437,7 +442,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
<Form {...addTargetForm}>
|
<Form {...addTargetForm}>
|
||||||
<form
|
<form
|
||||||
onSubmit={addTargetForm.handleSubmit(
|
onSubmit={addTargetForm.handleSubmit(
|
||||||
addTarget as any,
|
addTarget as any
|
||||||
)}
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
@ -452,11 +457,11 @@ export default function ReverseProxyTargets(props: {
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(
|
onValueChange={(
|
||||||
value,
|
value
|
||||||
) => {
|
) => {
|
||||||
addTargetForm.setValue(
|
addTargetForm.setValue(
|
||||||
"method",
|
"method",
|
||||||
value,
|
value
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -585,10 +590,10 @@ export default function ReverseProxyTargets(props: {
|
||||||
.column
|
.column
|
||||||
.columnDef
|
.columnDef
|
||||||
.header,
|
.header,
|
||||||
header.getContext(),
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
@ -607,7 +612,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
cell.column
|
cell.column
|
||||||
.columnDef
|
.columnDef
|
||||||
.cell,
|
.cell,
|
||||||
cell.getContext(),
|
cell.getContext()
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
@ -644,36 +649,36 @@ export default function ReverseProxyTargets(props: {
|
||||||
|
|
||||||
function isIPInSubnet(subnet: string, ip: string): boolean {
|
function isIPInSubnet(subnet: string, ip: string): boolean {
|
||||||
// Split subnet into IP and mask parts
|
// Split subnet into IP and mask parts
|
||||||
const [subnetIP, maskBits] = subnet.split('/');
|
const [subnetIP, maskBits] = subnet.split("/");
|
||||||
const mask = parseInt(maskBits);
|
const mask = parseInt(maskBits);
|
||||||
|
|
||||||
if (mask < 0 || mask > 32) {
|
if (mask < 0 || mask > 32) {
|
||||||
throw new Error('Invalid subnet mask. Must be between 0 and 32.');
|
throw new Error("Invalid subnet mask. Must be between 0 and 32.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert IP addresses to binary numbers
|
// Convert IP addresses to binary numbers
|
||||||
const subnetNum = ipToNumber(subnetIP);
|
const subnetNum = ipToNumber(subnetIP);
|
||||||
const ipNum = ipToNumber(ip);
|
const ipNum = ipToNumber(ip);
|
||||||
|
|
||||||
// Calculate subnet mask
|
// Calculate subnet mask
|
||||||
const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1);
|
const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1);
|
||||||
|
|
||||||
// Check if the IP is in the subnet
|
// Check if the IP is in the subnet
|
||||||
return (subnetNum & maskNum) === (ipNum & maskNum);
|
return (subnetNum & maskNum) === (ipNum & maskNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ipToNumber(ip: string): number {
|
function ipToNumber(ip: string): number {
|
||||||
// Validate IP address format
|
// Validate IP address format
|
||||||
const parts = ip.split('.');
|
const parts = ip.split(".");
|
||||||
if (parts.length !== 4) {
|
if (parts.length !== 4) {
|
||||||
throw new Error('Invalid IP address format');
|
throw new Error("Invalid IP address format");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert IP octets to 32-bit number
|
// Convert IP octets to 32-bit number
|
||||||
return parts.reduce((num, octet) => {
|
return parts.reduce((num, octet) => {
|
||||||
const oct = parseInt(octet);
|
const oct = parseInt(octet);
|
||||||
if (isNaN(oct) || oct < 0 || oct > 255) {
|
if (isNaN(oct) || oct < 0 || oct > 255) {
|
||||||
throw new Error('Invalid IP address octet');
|
throw new Error("Invalid IP address octet");
|
||||||
}
|
}
|
||||||
return (num << 8) + oct;
|
return (num << 8) + oct;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
|
@ -123,7 +123,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||||
<OrgProvider org={org}>
|
<OrgProvider org={org}>
|
||||||
<ResourceProvider resource={resource} authInfo={authInfo}>
|
<ResourceProvider resource={resource} authInfo={authInfo}>
|
||||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||||
<div className="mb-8 lg:max-w-2xl">
|
<div className="mb-8">
|
||||||
<ResourceInfoBox />
|
<ResourceInfoBox />
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -74,6 +74,43 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<ResourceRow>[] = [
|
const columns: ColumnDef<ResourceRow>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "dots",
|
||||||
|
header: "",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const resourceRow = row.original;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link
|
||||||
|
className="block w-full"
|
||||||
|
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||||
|
>
|
||||||
|
View settings
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedResource(resourceRow);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-red-500">Delete</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -214,55 +251,18 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex items-center justify-end">
|
||||||
<div className="flex items-center justify-end">
|
<Link
|
||||||
<DropdownMenu>
|
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||||
<DropdownMenuTrigger asChild>
|
>
|
||||||
<Button
|
<Button variant={"gray"} className="ml-2">
|
||||||
variant="ghost"
|
Edit
|
||||||
className="h-8 w-8 p-0"
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
>
|
</Button>
|
||||||
<span className="sr-only">
|
</Link>
|
||||||
Open menu
|
</div>
|
||||||
</span>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Link
|
|
||||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
|
||||||
>
|
|
||||||
View settings
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedResource(resourceRow);
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-red-500">
|
|
||||||
Delete
|
|
||||||
</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Link
|
|
||||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
|
||||||
>
|
|
||||||
<Button variant={"gray"} className="ml-2">
|
|
||||||
Edit
|
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
|
import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
|
||||||
import { constructShareLink } from "@app/lib/shareLinks";
|
import { constructShareLink } from "@app/lib/shareLinks";
|
||||||
import { ShareLinkRow } from "./ShareLinksTable";
|
import { ShareLinkRow } from "./ShareLinksTable";
|
||||||
import { QRCodeSVG } from "qrcode.react";
|
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -449,23 +449,23 @@ export default function CreateShareLinkForm({
|
||||||
{link && (
|
{link && (
|
||||||
<div className="max-w-md space-y-4">
|
<div className="max-w-md space-y-4">
|
||||||
<p>
|
<p>
|
||||||
You will only be able to see this link once.
|
You will only be able to see this link
|
||||||
Make sure to copy it.
|
once. Make sure to copy it.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Anyone with this link can access the
|
Anyone with this link can access the
|
||||||
resource. Share it with care.
|
resource. Share it with care.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="w-64 h-64 mx-auto flex items-center justify-center">
|
<div className="h-[250px] w-full mx-auto flex items-center justify-center">
|
||||||
<QRCodeSVG
|
<QRCodeCanvas value={link} size={200} />
|
||||||
value={link}
|
|
||||||
size={256}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<CopyTextBox text={link} wrapText={false} />
|
<CopyTextBox
|
||||||
|
text={link}
|
||||||
|
wrapText={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -473,8 +473,8 @@ export default function CreateShareLinkForm({
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="button"
|
||||||
form="share-link-form"
|
onClick={form.handleSubmit(onSubmit)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={link !== null || loading}
|
disabled={link !== null || loading}
|
||||||
>
|
>
|
||||||
|
|
|
@ -86,6 +86,48 @@ export default function ShareLinksTable({
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnDef<ShareLinkRow>[] = [
|
const columns: ColumnDef<ShareLinkRow>[] = [
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const resourceRow = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">
|
||||||
|
Open menu
|
||||||
|
</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
deleteSharelink(
|
||||||
|
resourceRow.accessTokenId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="text-red-500"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "resourceName",
|
accessorKey: "resourceName",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -236,48 +278,6 @@ export default function ShareLinksTable({
|
||||||
}
|
}
|
||||||
return "Never";
|
return "Never";
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "actions",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const resourceRow = row.original;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
<span className="sr-only">
|
|
||||||
Open menu
|
|
||||||
</span>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
deleteSharelink(
|
|
||||||
resourceRow.accessTokenId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="text-red-500"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -71,10 +71,48 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
|
|
||||||
const newRows = rows.filter((row) => row.id !== siteId);
|
const newRows = rows.filter((row) => row.id !== siteId);
|
||||||
|
|
||||||
|
setRows(newRows);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnDef<SiteRow>[] = [
|
const columns: ColumnDef<SiteRow>[] = [
|
||||||
|
{
|
||||||
|
id: "dots",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const siteRow = row.original;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link
|
||||||
|
className="block w-full"
|
||||||
|
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||||
|
>
|
||||||
|
View settings
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSite(siteRow);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-red-500">Delete</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -91,6 +129,41 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "online",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(column.getIsSorted() === "asc")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Online
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const originalRow = row.original;
|
||||||
|
|
||||||
|
if (originalRow.online) {
|
||||||
|
return (
|
||||||
|
<span className="text-green-500 flex items-center space-x-2">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span>Online</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span className="text-neutral-500 flex items-center space-x-2">
|
||||||
|
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||||
|
<span>Offline</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "nice",
|
accessorKey: "nice",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
|
@ -174,75 +247,12 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: "online",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Online
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const originalRow = row.original;
|
|
||||||
|
|
||||||
if (originalRow.online) {
|
|
||||||
return (
|
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
||||||
<span>Online</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<span className="text-gray-500 flex items-center space-x-2">
|
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
|
||||||
<span>Offline</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const siteRow = row.original;
|
const siteRow = row.original;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
||||||
<span className="sr-only">Open menu</span>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Link
|
|
||||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
|
||||||
>
|
|
||||||
View settings
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedSite(siteRow);
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-red-500">Delete</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Link
|
<Link
|
||||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||||
>
|
>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle,
|
||||||
} from "@app/components/ui/card";
|
} from "@app/components/ui/card";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { XCircle } from "lucide-react";
|
import { XCircle } from "lucide-react";
|
||||||
|
@ -20,7 +20,7 @@ type InviteStatusCardProps = {
|
||||||
|
|
||||||
export default function InviteStatusCard({
|
export default function InviteStatusCard({
|
||||||
type,
|
type,
|
||||||
token
|
token,
|
||||||
}: InviteStatusCardProps) {
|
}: InviteStatusCardProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
|
@ -22,7 +22,7 @@ import {
|
||||||
DrawerFooter,
|
DrawerFooter,
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
DrawerTrigger,
|
DrawerTrigger
|
||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
|
@ -43,6 +43,7 @@ const desktop = "(min-width: 768px)";
|
||||||
|
|
||||||
const Credenza = ({ children, ...props }: RootCredenzaProps) => {
|
const Credenza = ({ children, ...props }: RootCredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
// const isDesktop = true;
|
||||||
const Credenza = isDesktop ? Dialog : Drawer;
|
const Credenza = isDesktop ? Dialog : Drawer;
|
||||||
|
|
||||||
return <Credenza {...props}>{children}</Credenza>;
|
return <Credenza {...props}>{children}</Credenza>;
|
||||||
|
@ -50,6 +51,8 @@ const Credenza = ({ children, ...props }: RootCredenzaProps) => {
|
||||||
|
|
||||||
const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
// const isDesktop = true;
|
||||||
|
|
||||||
const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -61,10 +64,12 @@ const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
|
||||||
const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
// const isDesktop = true;
|
||||||
|
|
||||||
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaClose className={className} {...props}>
|
<CredenzaClose className={cn("mb-3 md:mb-0", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
);
|
);
|
||||||
|
@ -72,6 +77,8 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
|
||||||
const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
// const isDesktop = true;
|
||||||
|
|
||||||
const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
|
const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -87,6 +94,8 @@ const CredenzaDescription = ({
|
||||||
...props
|
...props
|
||||||
}: CredenzaProps) => {
|
}: CredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
// const isDesktop = true;
|
||||||
|
|
||||||
const CredenzaDescription = isDesktop
|
const CredenzaDescription = isDesktop
|
||||||
? DialogDescription
|
? DialogDescription
|
||||||
: DrawerDescription;
|
: DrawerDescription;
|
||||||
|
@ -100,6 +109,8 @@ const CredenzaDescription = ({
|
||||||
|
|
||||||
const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
// const isDesktop = true;
|
||||||
|
|
||||||
const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
|
const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -111,6 +122,8 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
|
|
||||||
const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
// const isDesktop = true;
|
||||||
|
|
||||||
const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
|
const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -126,10 +139,19 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className={cn("px-0 mb-4", className)} {...props}>
|
||||||
|
// {children}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery(desktop);
|
const isDesktop = useMediaQuery(desktop);
|
||||||
|
// const isDesktop = true;
|
||||||
|
|
||||||
const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
|
const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -148,5 +170,5 @@ export {
|
||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle,
|
CredenzaTitle,
|
||||||
CredenzaBody,
|
CredenzaBody,
|
||||||
CredenzaFooter,
|
CredenzaFooter
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,7 +38,7 @@ import {
|
||||||
import { useToast } from "@app/hooks/useToast";
|
import { useToast } from "@app/hooks/useToast";
|
||||||
import { formatAxiosError } from "@app/lib/utils";
|
import { formatAxiosError } from "@app/lib/utils";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
import { QRCodeSVG } from "qrcode.react";
|
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
|
||||||
const enableSchema = z.object({
|
const enableSchema = z.object({
|
||||||
|
@ -221,15 +221,10 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
|
||||||
Scan this QR code with your authenticator app or
|
Scan this QR code with your authenticator app or
|
||||||
enter the secret key manually:
|
enter the secret key manually:
|
||||||
</p>
|
</p>
|
||||||
<div className="w-64 h-64 mx-auto flex items-center justify-center">
|
<div className="h-[250px] mx-auto flex items-center justify-center">
|
||||||
<QRCodeSVG value={secretUri} size={256} />
|
<QRCodeCanvas value={secretUri} size={200} />
|
||||||
</div>
|
|
||||||
<div className="max-w-md mx-auto">
|
|
||||||
<CopyTextBox
|
|
||||||
text={secretKey}
|
|
||||||
wrapText={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<CopyTextBox text={secretKey} wrapText={false} />
|
||||||
|
|
||||||
<Form {...confirmForm}>
|
<Form {...confirmForm}>
|
||||||
<form
|
<form
|
||||||
|
@ -288,10 +283,16 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
{(step === 1 || step === 2) && (
|
{(step === 1 || step === 2) && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="button"
|
||||||
form="form"
|
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
if (step === 1) {
|
||||||
|
enableForm.handleSubmit(request2fa)();
|
||||||
|
} else {
|
||||||
|
confirmForm.handleSubmit(confirm2fa)();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -21,9 +21,9 @@ export function SidebarSettings({
|
||||||
limitWidth,
|
limitWidth,
|
||||||
}: SideBarSettingsProps) {
|
}: SideBarSettingsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 0 pb-16k">
|
<div className="space-y-8 pb-16k">
|
||||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-32 lg:space-y-0">
|
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-32 lg:space-y-0">
|
||||||
<aside className="-mx-4 lg:w-1/5">
|
<aside className="lg:w-1/5">
|
||||||
<SidebarNav items={sidebarNavItems} disabled={disabled} />
|
<SidebarNav items={sidebarNavItems} disabled={disabled} />
|
||||||
</aside>
|
</aside>
|
||||||
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}>
|
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, usePathname, useRouter } from "next/navigation";
|
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
@ -35,6 +35,12 @@ export function SidebarNav({
|
||||||
const resourceId = params.resourceId as string;
|
const resourceId = params.resourceId as string;
|
||||||
const userId = params.userId as string;
|
const userId = params.userId as string;
|
||||||
|
|
||||||
|
const [selectedValue, setSelectedValue] = React.useState<string>(getSelectedValue());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedValue(getSelectedValue());
|
||||||
|
}, [usePathname()]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSelectChange = (value: string) => {
|
const handleSelectChange = (value: string) => {
|
||||||
|
@ -58,9 +64,10 @@ export function SidebarNav({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="block lg:hidden px-4">
|
<div className="block lg:hidden">
|
||||||
<Select
|
<Select
|
||||||
defaultValue={getSelectedValue()}
|
defaultValue={selectedValue}
|
||||||
|
value={selectedValue}
|
||||||
onValueChange={handleSelectChange}
|
onValueChange={handleSelectChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
|
|
@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
|
||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-11 w-full rounded-md bg-transparent py-3 text-base md:text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/30 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -15,7 +15,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
<input
|
<input
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -39,7 +39,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
Loading…
Add table
Reference in a new issue