improve spacing and column layout on mobile

This commit is contained in:
Milo Schwartz 2024-12-29 22:07:12 -05:00
parent 152a62a27b
commit 32ffb33d98
No known key found for this signature in database
20 changed files with 400 additions and 346 deletions

View file

@ -8,8 +8,9 @@ export function createApiClient({ env }: { env: env }): AxiosInstance {
return apiInstance;
}
if (apiInstance) {
return apiInstance
if (typeof window === "undefined") {
// @ts-ignore
return;
}
let baseURL;
@ -45,7 +46,8 @@ export const internal = axios.create({
baseURL: `http://localhost:${process.env.SERVER_EXTERNAL_PORT}/api/v1`,
timeout: 10000,
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
"X-CSRF-Token": "x-csrf-protection"
}
});

View file

@ -40,26 +40,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
const { toast } = useToast();
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",
cell: ({ row }) => {
@ -67,14 +47,9 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
return (
<>
<div className="flex items-center justify-end">
<div>
{roleRow.isAdmin && (
<Button
variant="ghost"
className="h-8 w-8 p-0 opacity-0 cursor-default"
>
Placeholder
</Button>
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!roleRow.isAdmin && (
<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"
}
];

View file

@ -50,6 +50,64 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const { toast } = useToast();
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",
header: ({ column }) => {
@ -114,73 +172,27 @@ export default function UsersTable({ users: u }: UsersTableProps) {
id: "actions",
cell: ({ row }) => {
const userRow = row.original;
return (
<>
<div className="flex items-center justify-end">
{userRow.isOwner && (
<Button
variant="ghost"
className="opacity-0 cursor-default"
>
Placeholder
<div className="flex items-center justify-end">
{userRow.isOwner && (
<Button
variant="ghost"
className="opacity-0 cursor-default"
>
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>
)}
{!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}`}
>
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>
</>
</Link>
)}
</div>
);
}
}

View file

@ -69,7 +69,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
)}
</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" />
<a
href={fullUrl}

View file

@ -9,7 +9,7 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectValue
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { AxiosResponse } from "axios";
@ -24,7 +24,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage,
FormMessage
} from "@app/components/ui/form";
import { CreateTargetResponse } from "@server/routers/target";
import {
@ -34,7 +34,7 @@ import {
getPaginationRowModel,
getCoreRowModel,
useReactTable,
flexRender,
flexRender
} from "@tanstack/react-table";
import {
Table,
@ -42,7 +42,7 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow,
TableRow
} from "@app/components/ui/table";
import { useToast } from "@app/hooks/useToast";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
@ -59,9 +59,9 @@ const addTargetSchema = z.object({
port: z
.string()
.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(),
});
@ -99,16 +99,16 @@ export default function ReverseProxyTargets(props: {
defaultValues: {
ip: "",
method: "http",
port: "80",
port: "80"
// protocol: "TCP",
},
}
});
useEffect(() => {
const fetchTargets = async () => {
try {
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
`/resource/${params.resourceId}/targets`,
`/resource/${params.resourceId}/targets`
);
if (res.status === 200) {
@ -121,8 +121,8 @@ export default function ReverseProxyTargets(props: {
title: "Failed to fetch targets",
description: formatAxiosError(
err,
"An error occurred while fetching targets",
),
"An error occurred while fetching targets"
)
});
} finally {
setPageLoading(false);
@ -133,7 +133,7 @@ export default function ReverseProxyTargets(props: {
const fetchSite = async () => {
try {
const res = await api.get<AxiosResponse<GetSiteResponse>>(
`/site/${resource.siteId}`,
`/site/${resource.siteId}`
);
if (res.status === 200) {
@ -146,27 +146,28 @@ export default function ReverseProxyTargets(props: {
title: "Failed to fetch resource",
description: formatAxiosError(
err,
"An error occurred while fetching resource",
),
"An error occurred while fetching resource"
)
});
}
}
};
fetchSite();
}, []);
async function addTarget(data: AddTargetFormValues) {
// Check if target with same IP, port and method already exists
const isDuplicate = targets.some(
target => target.ip === data.ip &&
target.port === data.port &&
target.method === data.method
(target) =>
target.ip === data.ip &&
target.port === data.port &&
target.method === data.method
);
if (isDuplicate) {
toast({
variant: "destructive",
title: "Duplicate target",
description: "A target with these settings already exists",
description: "A target with these settings already exists"
});
return;
}
@ -179,7 +180,7 @@ export default function ReverseProxyTargets(props: {
toast({
variant: "destructive",
title: "Invalid target IP",
description: "Target IP must be within the site subnet",
description: "Target IP must be within the site subnet"
});
return;
}
@ -190,7 +191,7 @@ export default function ReverseProxyTargets(props: {
enabled: true,
targetId: new Date().getTime(),
new: true,
resourceId: resource.resourceId,
resourceId: resource.resourceId
};
setTargets([...targets, newTarget]);
@ -199,7 +200,7 @@ export default function ReverseProxyTargets(props: {
const removeTarget = (targetId: number) => {
setTargets([
...targets.filter((target) => target.targetId !== targetId),
...targets.filter((target) => target.targetId !== targetId)
]);
if (!targets.find((target) => target.targetId === targetId)?.new) {
@ -212,8 +213,8 @@ export default function ReverseProxyTargets(props: {
targets.map((target) =>
target.targetId === targetId
? { ...target, ...data, updated: true }
: target,
),
: target
)
);
}
@ -222,7 +223,7 @@ export default function ReverseProxyTargets(props: {
setLoading(true);
const res = await api.post(`/resource/${params.resourceId}`, {
ssl: sslEnabled,
ssl: sslEnabled
});
updateResource({ ssl: sslEnabled });
@ -233,7 +234,7 @@ export default function ReverseProxyTargets(props: {
port: target.port,
// protocol: target.protocol,
method: target.method,
enabled: target.enabled,
enabled: target.enabled
};
if (target.new) {
@ -244,7 +245,7 @@ export default function ReverseProxyTargets(props: {
} else if (target.updated) {
const res = await api.post(
`/target/${target.targetId}`,
data,
data
);
}
@ -253,23 +254,23 @@ export default function ReverseProxyTargets(props: {
let res = {
...t,
new: false,
updated: false,
updated: false
};
return res;
}),
})
]);
}
for (const targetId of targetsToRemove) {
await api.delete(`/target/${targetId}`);
setTargets(
targets.filter((target) => target.targetId !== targetId),
targets.filter((target) => target.targetId !== targetId)
);
}
toast({
title: "Resource updated",
description: "Resource and targets updated successfully",
description: "Resource and targets updated successfully"
});
setTargetsToRemove([]);
@ -280,8 +281,8 @@ export default function ReverseProxyTargets(props: {
title: "Operation failed",
description: formatAxiosError(
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 })
}
>
<SelectTrigger>{row.original.method}</SelectTrigger>
<SelectTrigger className="min-w-[100px]">
{row.original.method}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
</SelectContent>
</Select>
),
)
},
{
accessorKey: "ip",
@ -313,13 +316,14 @@ export default function ReverseProxyTargets(props: {
cell: ({ row }) => (
<Input
defaultValue={row.original.ip}
className="min-w-[150px]"
onBlur={(e) =>
updateTarget(row.original.targetId, {
ip: e.target.value,
ip: e.target.value
})
}
/>
),
)
},
{
accessorKey: "port",
@ -328,13 +332,14 @@ export default function ReverseProxyTargets(props: {
<Input
type="number"
defaultValue={row.original.port}
className="min-w-[100px]"
onBlur={(e) =>
updateTarget(row.original.targetId, {
port: parseInt(e.target.value, 10),
port: parseInt(e.target.value, 10)
})
}
/>
),
)
},
// {
// accessorKey: "protocol",
@ -364,7 +369,7 @@ export default function ReverseProxyTargets(props: {
updateTarget(row.original.targetId, { enabled: val })
}
/>
),
)
},
{
id: "actions",
@ -387,8 +392,8 @@ export default function ReverseProxyTargets(props: {
</Button>
</div>
</>
),
},
)
}
];
const table = useReactTable({
@ -397,7 +402,7 @@ export default function ReverseProxyTargets(props: {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getFilteredRowModel: getFilteredRowModel()
});
if (pageLoading) {
@ -437,7 +442,7 @@ export default function ReverseProxyTargets(props: {
<Form {...addTargetForm}>
<form
onSubmit={addTargetForm.handleSubmit(
addTarget as any,
addTarget as any
)}
className="space-y-4"
>
@ -452,11 +457,11 @@ export default function ReverseProxyTargets(props: {
<Select
{...field}
onValueChange={(
value,
value
) => {
addTargetForm.setValue(
"method",
value,
value
);
}}
>
@ -585,10 +590,10 @@ export default function ReverseProxyTargets(props: {
.column
.columnDef
.header,
header.getContext(),
header.getContext()
)}
</TableHead>
),
)
)}
</TableRow>
))}
@ -607,7 +612,7 @@ export default function ReverseProxyTargets(props: {
cell.column
.columnDef
.cell,
cell.getContext(),
cell.getContext()
)}
</TableCell>
))}
@ -644,36 +649,36 @@ export default function ReverseProxyTargets(props: {
function isIPInSubnet(subnet: string, ip: string): boolean {
// Split subnet into IP and mask parts
const [subnetIP, maskBits] = subnet.split('/');
const [subnetIP, maskBits] = subnet.split("/");
const mask = parseInt(maskBits);
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
const subnetNum = ipToNumber(subnetIP);
const ipNum = ipToNumber(ip);
// Calculate subnet mask
const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1);
// Check if the IP is in the subnet
return (subnetNum & maskNum) === (ipNum & maskNum);
}
function ipToNumber(ip: string): number {
// Validate IP address format
const parts = ip.split('.');
const parts = ip.split(".");
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
return parts.reduce((num, octet) => {
const oct = parseInt(octet);
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;
}, 0);

View file

@ -123,7 +123,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
<OrgProvider org={org}>
<ResourceProvider resource={resource} authInfo={authInfo}>
<SidebarSettings sidebarNavItems={sidebarNavItems}>
<div className="mb-8 lg:max-w-2xl">
<div className="mb-8">
<ResourceInfoBox />
</div>
{children}

View file

@ -74,6 +74,43 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
};
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",
header: ({ column }) => {
@ -214,55 +251,18 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
{
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>
<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>
</>
<div className="flex items-center justify-end">
<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>
);
}
}

View file

@ -63,7 +63,7 @@ import { Checkbox } from "@app/components/ui/checkbox";
import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
import { constructShareLink } from "@app/lib/shareLinks";
import { ShareLinkRow } from "./ShareLinksTable";
import { QRCodeSVG } from "qrcode.react";
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
type FormProps = {
open: boolean;
@ -449,23 +449,23 @@ export default function CreateShareLinkForm({
{link && (
<div className="max-w-md space-y-4">
<p>
You will only be able to see this link once.
Make sure to copy it.
You will only be able to see this link
once. Make sure to copy it.
</p>
<p>
Anyone with this link can access the
resource. Share it with care.
</p>
<div className="w-64 h-64 mx-auto flex items-center justify-center">
<QRCodeSVG
value={link}
size={256}
/>
<div className="h-[250px] w-full mx-auto flex items-center justify-center">
<QRCodeCanvas value={link} size={200} />
</div>
<div className="mx-auto">
<CopyTextBox text={link} wrapText={false} />
<CopyTextBox
text={link}
wrapText={false}
/>
</div>
</div>
)}
@ -473,8 +473,8 @@ export default function CreateShareLinkForm({
</CredenzaBody>
<CredenzaFooter>
<Button
type="submit"
form="share-link-form"
type="button"
onClick={form.handleSubmit(onSubmit)}
loading={loading}
disabled={link !== null || loading}
>

View file

@ -86,6 +86,48 @@ export default function ShareLinksTable({
}
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",
header: ({ column }) => {
@ -236,48 +278,6 @@ export default function ShareLinksTable({
}
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>
</>
);
}
}
];

View file

@ -71,10 +71,48 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
setIsDeleteModalOpen(false);
const newRows = rows.filter((row) => row.id !== siteId);
setRows(newRows);
});
};
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",
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",
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",
cell: ({ row }) => {
const router = useRouter();
const siteRow = 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>
<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
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>

View file

@ -7,7 +7,7 @@ import {
CardContent,
CardFooter,
CardHeader,
CardTitle
CardTitle,
} from "@app/components/ui/card";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { XCircle } from "lucide-react";
@ -20,7 +20,7 @@ type InviteStatusCardProps = {
export default function InviteStatusCard({
type,
token
token,
}: InviteStatusCardProps) {
const router = useRouter();

View file

@ -12,7 +12,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogTrigger
} from "@/components/ui/dialog";
import {
Drawer,
@ -22,7 +22,7 @@ import {
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
DrawerTrigger
} from "@/components/ui/drawer";
interface BaseProps {
@ -43,6 +43,7 @@ const desktop = "(min-width: 768px)";
const Credenza = ({ children, ...props }: RootCredenzaProps) => {
const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const Credenza = isDesktop ? Dialog : Drawer;
return <Credenza {...props}>{children}</Credenza>;
@ -50,6 +51,8 @@ const Credenza = ({ children, ...props }: RootCredenzaProps) => {
const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
return (
@ -61,10 +64,12 @@ const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
return (
<CredenzaClose className={className} {...props}>
<CredenzaClose className={cn("mb-3 md:mb-0", className)} {...props}>
{children}
</CredenzaClose>
);
@ -72,6 +77,8 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
return (
@ -87,6 +94,8 @@ const CredenzaDescription = ({
...props
}: CredenzaProps) => {
const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const CredenzaDescription = isDesktop
? DialogDescription
: DrawerDescription;
@ -100,6 +109,8 @@ const CredenzaDescription = ({
const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
return (
@ -111,6 +122,8 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
return (
@ -126,10 +139,19 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
{children}
</div>
);
// return (
// <div className={cn("px-0 mb-4", className)} {...props}>
// {children}
// </div>
// );
};
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
const isDesktop = useMediaQuery(desktop);
// const isDesktop = true;
const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
return (
@ -148,5 +170,5 @@ export {
CredenzaHeader,
CredenzaTitle,
CredenzaBody,
CredenzaFooter,
CredenzaFooter
};

View file

@ -38,7 +38,7 @@ import {
import { useToast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/utils";
import CopyTextBox from "@app/components/CopyTextBox";
import { QRCodeSVG } from "qrcode.react";
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
import { useUserContext } from "@app/hooks/useUserContext";
const enableSchema = z.object({
@ -221,15 +221,10 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
Scan this QR code with your authenticator app or
enter the secret key manually:
</p>
<div className="w-64 h-64 mx-auto flex items-center justify-center">
<QRCodeSVG value={secretUri} size={256} />
</div>
<div className="max-w-md mx-auto">
<CopyTextBox
text={secretKey}
wrapText={false}
/>
<div className="h-[250px] mx-auto flex items-center justify-center">
<QRCodeCanvas value={secretUri} size={200} />
</div>
<CopyTextBox text={secretKey} wrapText={false} />
<Form {...confirmForm}>
<form
@ -288,10 +283,16 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
<CredenzaFooter>
{(step === 1 || step === 2) && (
<Button
type="submit"
form="form"
type="button"
loading={loading}
disabled={loading}
onClick={() => {
if (step === 1) {
enableForm.handleSubmit(request2fa)();
} else {
confirmForm.handleSubmit(confirm2fa)();
}
}}
>
Submit
</Button>

View file

@ -21,9 +21,9 @@ export function SidebarSettings({
limitWidth,
}: SideBarSettingsProps) {
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">
<aside className="-mx-4 lg:w-1/5">
<aside className="lg:w-1/5">
<SidebarNav items={sidebarNavItems} disabled={disabled} />
</aside>
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}>

View file

@ -1,6 +1,6 @@
"use client";
import React from "react";
import React, { useEffect } from "react";
import Link from "next/link";
import { useParams, usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
@ -35,6 +35,12 @@ export function SidebarNav({
const resourceId = params.resourceId as string;
const userId = params.userId as string;
const [selectedValue, setSelectedValue] = React.useState<string>(getSelectedValue());
useEffect(() => {
setSelectedValue(getSelectedValue());
}, [usePathname()]);
const router = useRouter();
const handleSelectChange = (value: string) => {
@ -58,9 +64,10 @@ export function SidebarNav({
return (
<div>
<div className="block lg:hidden px-4">
<div className="block lg:hidden">
<Select
defaultValue={getSelectedValue()}
defaultValue={selectedValue}
value={selectedValue}
onValueChange={handleSelectChange}
disabled={disabled}
>

View file

@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
<CommandPrimitive.Input
ref={ref}
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
)}
{...props}

View file

@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
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
)}
{...props}

View file

@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
<div
ref={ref}
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",
className
)}

View file

@ -15,7 +15,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={showPassword ? "text" : "password"}
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
)}
ref={ref}
@ -39,7 +39,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
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
)}
ref={ref}

View file

@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
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
)}
{...props}