append site name to resource in list add add site to info card closes #297

This commit is contained in:
miloschwartz 2025-03-08 22:10:44 -05:00
parent 767fec19cd
commit 8ec55eb70d
No known key found for this signature in database
8 changed files with 83 additions and 50 deletions

View file

@ -5,7 +5,8 @@ import {
resources, resources,
userResources, userResources,
roleResources, roleResources,
resourceAccessToken resourceAccessToken,
sites
} from "@server/db/schema"; } from "@server/db/schema";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -59,7 +60,8 @@ function queryAccessTokens(
title: resourceAccessToken.title, title: resourceAccessToken.title,
description: resourceAccessToken.description, description: resourceAccessToken.description,
createdAt: resourceAccessToken.createdAt, createdAt: resourceAccessToken.createdAt,
resourceName: resources.name resourceName: resources.name,
siteName: sites.name
}; };
if (orgId) { if (orgId) {
@ -70,6 +72,10 @@ function queryAccessTokens(
resources, resources,
eq(resourceAccessToken.resourceId, resources.resourceId) eq(resourceAccessToken.resourceId, resources.resourceId)
) )
.leftJoin(
sites,
eq(resources.resourceId, sites.siteId)
)
.where( .where(
and( and(
inArray( inArray(
@ -91,6 +97,10 @@ function queryAccessTokens(
resources, resources,
eq(resourceAccessToken.resourceId, resources.resourceId) eq(resourceAccessToken.resourceId, resources.resourceId)
) )
.leftJoin(
sites,
eq(resources.resourceId, sites.siteId)
)
.where( .where(
and( and(
inArray( inArray(

View file

@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { Resource, resources } from "@server/db/schema"; import { Resource, resources, sites } from "@server/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@ -18,7 +18,9 @@ const getResourceSchema = z
}) })
.strict(); .strict();
export type GetResourceResponse = Resource; export type GetResourceResponse = Resource & {
siteName: string;
};
export async function getResource( export async function getResource(
req: Request, req: Request,
@ -38,13 +40,17 @@ export async function getResource(
const { resourceId } = parsedParams.data; const { resourceId } = parsedParams.data;
const resource = await db const [resp] = await db
.select() .select()
.from(resources) .from(resources)
.where(eq(resources.resourceId, resourceId)) .where(eq(resources.resourceId, resourceId))
.leftJoin(sites, eq(sites.siteId, resources.siteId))
.limit(1); .limit(1);
if (resource.length === 0) { const resource = resp.resources;
const site = resp.sites;
if (!resource) {
return next( return next(
createHttpError( createHttpError(
HttpCode.NOT_FOUND, HttpCode.NOT_FOUND,
@ -54,7 +60,10 @@ export async function getResource(
} }
return response(res, { return response(res, {
data: resource[0], data: {
...resource,
siteName: site?.name
},
success: true, success: true,
error: false, error: false,
message: "Resource retrieved successfully", message: "Resource retrieved successfully",

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react"; import { ArrowRight, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
import { useResourceContext } from "@app/hooks/useResourceContext"; import { useResourceContext } from "@app/hooks/useResourceContext";
import { Separator } from "@app/components/ui/separator"; import { Separator } from "@app/components/ui/separator";
import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyToClipboard from "@app/components/CopyToClipboard";
@ -11,6 +11,7 @@ import {
InfoSections, InfoSections,
InfoSectionTitle InfoSectionTitle
} from "@app/components/InfoSection"; } from "@app/components/InfoSection";
import Link from "next/link";
type ResourceInfoBoxType = {}; type ResourceInfoBoxType = {};
@ -26,7 +27,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
Resource Information Resource Information
</AlertTitle> </AlertTitle>
<AlertDescription className="mt-4"> <AlertDescription className="mt-4">
<InfoSections> <InfoSections cols={3}>
{resource.http ? ( {resource.http ? (
<> <>
<InfoSection> <InfoSection>
@ -40,22 +41,16 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
authInfo.whitelist ? ( authInfo.whitelist ? (
<div className="flex items-start space-x-2 text-green-500"> <div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" /> <ShieldCheck className="w-4 h-4 mt-0.5" />
<span> <span>Protected</span>
This resource is protected with
at least one authentication method.
</span>
</div> </div>
) : ( ) : (
<div className="flex items-center space-x-2 text-yellow-500"> <div className="flex items-center space-x-2 text-yellow-500">
<ShieldOff className="w-4 h-4" /> <ShieldOff className="w-4 h-4" />
<span> <span>Not Protected</span>
Anyone can access this resource.
</span>
</div> </div>
)} )}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<Separator orientation="vertical" />
<InfoSection> <InfoSection>
<InfoSectionTitle>URL</InfoSectionTitle> <InfoSectionTitle>URL</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
@ -65,6 +60,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
/> />
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection>
<InfoSectionTitle>Site</InfoSectionTitle>
<InfoSectionContent>
{resource.siteName}
</InfoSectionContent>
</InfoSection>
</> </>
) : ( ) : (
<> <>
@ -76,7 +77,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</span> </span>
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<Separator orientation="vertical" />
<InfoSection> <InfoSection>
<InfoSectionTitle>Port</InfoSectionTitle> <InfoSectionTitle>Port</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>

View file

@ -265,6 +265,12 @@ export default function GeneralForm() {
description: "The resource has been transferred successfully" description: "The resource has been transferred successfully"
}); });
router.refresh(); router.refresh();
updateResource({
siteName:
sites.find((site) => site.siteId === data.siteId)?.name ||
""
});
} }
setTransferLoading(false); setTransferLoading(false);
} }
@ -606,9 +612,7 @@ export default function GeneralForm() {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0"> <PopoverContent className="w-full p-0">
<Command> <Command>
<CommandInput <CommandInput placeholder="Search sites" />
placeholder="Search sites"
/>
<CommandEmpty> <CommandEmpty>
No sites found. No sites found.
</CommandEmpty> </CommandEmpty>

View file

@ -107,7 +107,12 @@ export default function CreateShareLinkForm({
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [resources, setResources] = useState< const [resources, setResources] = useState<
{ resourceId: number; name: string; resourceUrl: string }[] {
resourceId: number;
name: string;
resourceUrl: string;
siteName: string | null;
}[]
>([]); >([]);
const timeUnits = [ const timeUnits = [
@ -159,7 +164,8 @@ export default function CreateShareLinkForm({
.map((r) => ({ .map((r) => ({
resourceId: r.resourceId, resourceId: r.resourceId,
name: r.name, name: r.name,
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/` resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`,
siteName: r.siteName
})) }))
); );
} }
@ -231,19 +237,28 @@ export default function CreateShareLinkForm({
token.accessToken token.accessToken
); );
setDirectLink(directLink); setDirectLink(directLink);
const resource = resources.find((r) => r.resourceId === values.resourceId);
onCreated?.({ onCreated?.({
accessTokenId: token.accessTokenId, accessTokenId: token.accessTokenId,
resourceId: token.resourceId, resourceId: token.resourceId,
resourceName: values.resourceName, resourceName: values.resourceName,
title: token.title, title: token.title,
createdAt: token.createdAt, createdAt: token.createdAt,
expiresAt: token.expiresAt expiresAt: token.expiresAt,
siteName: resource?.siteName || null,
}); });
} }
setLoading(false); setLoading(false);
} }
function getSelectedResourceName(id: number) {
const resource = resources.find((r) => r.resourceId === id);
return `${resource?.name} ${resource?.siteName ? `(${resource.siteName})` : ""}`;
}
return ( return (
<> <>
<Credenza <Credenza
@ -292,14 +307,9 @@ export default function CreateShareLinkForm({
)} )}
> >
{field.value {field.value
? resources.find( ? getSelectedResourceName(
( field.value
r
) =>
r.resourceId ===
field.value
) )
?.name
: "Select resource"} : "Select resource"}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@ -348,9 +358,7 @@ export default function CreateShareLinkForm({
: "opacity-0" : "opacity-0"
)} )}
/> />
{ {`${r.name} ${r.siteName ? `(${r.siteName})` : ""}`}
r.name
}
</CommandItem> </CommandItem>
) )
)} )}

View file

@ -41,6 +41,7 @@ export type ShareLinkRow = {
title: string | null; title: string | null;
createdAt: number; createdAt: number;
expiresAt: number | null; expiresAt: number | null;
siteName: string | null;
}; };
type ShareLinksTableProps = { type ShareLinksTableProps = {
@ -145,7 +146,8 @@ export default function ShareLinksTable({
return ( return (
<Link href={`/${orgId}/settings/resources/${r.resourceId}`}> <Link href={`/${orgId}/settings/resources/${r.resourceId}`}>
<Button variant="outline"> <Button variant="outline">
{r.resourceName} {r.resourceName}{" "}
{r.siteName ? `(${r.siteName})` : ""}
<ArrowUpRight className="ml-2 h-4 w-4" /> <ArrowUpRight className="ml-2 h-4 w-4" />
</Button> </Button>
</Link> </Link>
@ -274,20 +276,21 @@ export default function ShareLinksTable({
return "Never"; return "Never";
} }
}, },
{ {
id: "delete", id: "delete",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="flex items-center justify-end space-x-2"> <div className="flex items-center justify-end space-x-2">
<Button <Button
variant="outlinePrimary" variant="outlinePrimary"
onClick={() => deleteSharelink(row.original.accessTokenId)} onClick={() =>
deleteSharelink(row.original.accessTokenId)
}
> >
Delete Delete
</Button> </Button>
</div> </div>
) )
} }
]; ];
return ( return (

View file

@ -3,7 +3,6 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { InfoIcon } from "lucide-react"; import { InfoIcon } from "lucide-react";
import { useSiteContext } from "@app/hooks/useSiteContext"; import { useSiteContext } from "@app/hooks/useSiteContext";
import { Separator } from "@app/components/ui/separator";
import { import {
InfoSection, InfoSection,
InfoSectionContent, InfoSectionContent,
@ -33,7 +32,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
<InfoIcon className="h-4 w-4" /> <InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">Site Information</AlertTitle> <AlertTitle className="font-semibold">Site Information</AlertTitle>
<AlertDescription className="mt-4"> <AlertDescription className="mt-4">
<InfoSections> <InfoSections cols={2}>
{(site.type == "newt" || site.type == "wireguard") && ( {(site.type == "newt" || site.type == "wireguard") && (
<> <>
<InfoSection> <InfoSection>
@ -52,8 +51,6 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
)} )}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<Separator orientation="vertical" />
</> </>
)} )}
<InfoSection> <InfoSection>

View file

@ -1,8 +1,16 @@
"use client"; "use client";
export function InfoSections({ children }: { children: React.ReactNode }) { export function InfoSections({
children,
cols
}: {
children: React.ReactNode;
cols?: number;
}) {
return ( return (
<div className="grid grid-cols-1 md:gap-4 gap-2 md:grid-cols-[1fr_auto_1fr] md:items-start"> <div
className={`grid md:grid-cols-${cols || 1} md:gap-4 gap-2 md:items-start grid-cols-1`}
>
{children} {children}
</div> </div>
); );
@ -23,9 +31,3 @@ export function InfoSectionContent({
}) { }) {
return <div className="break-words">{children}</div>; return <div className="break-words">{children}</div>;
} }
export function Divider() {
return (
<div className="hidden md:block border-l border-gray-300 h-auto mx-4"></div>
);
}