add site resource modes and alias

This commit is contained in:
miloschwartz
2025-11-05 15:24:07 -08:00
parent e51b6b545e
commit 85892c30b2
16 changed files with 711 additions and 382 deletions

View File

@@ -1545,6 +1545,17 @@
"editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
"editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
"editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
"editInternalResourceDialogPortModeRequired": "Protocol, proxy port, and destination port are required for port mode",
"editInternalResourceDialogMode": "Mode",
"editInternalResourceDialogModePort": "Port",
"editInternalResourceDialogModeHost": "Host",
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogDestination": "Destination",
"editInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
"editInternalResourceDialogDestinationIPDescription": "The IP or hostname address of the resource on the site's network.",
"editInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "An optional alias for this resource.",
"createInternalResourceDialogNoSitesAvailable": "No Sites Available", "createInternalResourceDialogNoSitesAvailable": "No Sites Available",
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
"createInternalResourceDialogClose": "Close", "createInternalResourceDialogClose": "Close",
@@ -1578,6 +1589,16 @@
"createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
"createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
"createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
"createInternalResourceDialogPortModeRequired": "Protocol, proxy port, and destination port are required for port mode",
"createInternalResourceDialogMode": "Mode",
"createInternalResourceDialogModePort": "Port",
"createInternalResourceDialogModeHost": "Host",
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogDestination": "Destination",
"createInternalResourceDialogDestinationHostDescription": "The IP address or hostname of the resource on the site's network.",
"createInternalResourceDialogDestinationCidrDescription": "The CIDR range of the resource on the site's network.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAliasDescription": "An optional alias for this resource.",
"siteConfiguration": "Configuration", "siteConfiguration": "Configuration",
"siteAcceptClientConnections": "Accept Client Connections", "siteAcceptClientConnections": "Accept Client Connections",
"siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.",

View File

@@ -204,11 +204,13 @@ export const siteResources = pgTable("siteResources", {
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
niceId: varchar("niceId").notNull(), niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(), name: varchar("name").notNull(),
protocol: varchar("protocol").notNull(), mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
proxyPort: integer("proxyPort").notNull(), protocol: varchar("protocol"), // only for port mode
destinationPort: integer("destinationPort").notNull(), proxyPort: integer("proxyPort"), // only for port mode
destinationIp: varchar("destinationIp").notNull(), destinationPort: integer("destinationPort"), // only for port mode
enabled: boolean("enabled").notNull().default(true) destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
enabled: boolean("enabled").notNull().default(true),
alias: varchar("alias")
}); });
export const roleSiteResources = pgTable("roleSiteResources", { export const roleSiteResources = pgTable("roleSiteResources", {

View File

@@ -225,11 +225,13 @@ export const siteResources = sqliteTable("siteResources", {
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, { onDelete: "cascade" }),
niceId: text("niceId").notNull(), niceId: text("niceId").notNull(),
name: text("name").notNull(), name: text("name").notNull(),
protocol: text("protocol").notNull(), mode: text("mode").notNull(), // "host" | "cidr" | "port"
proxyPort: integer("proxyPort").notNull(), protocol: text("protocol"), // only for port mode
destinationPort: integer("destinationPort").notNull(), proxyPort: integer("proxyPort"), // only for port mode
destinationIp: text("destinationIp").notNull(), destinationPort: integer("destinationPort"), // only for port mode
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) destination: text("destination").notNull(), // ip, cidr, hostname
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
alias: text("alias")
}); });
export const roleSiteResources = sqliteTable("roleSiteResources", { export const roleSiteResources = sqliteTable("roleSiteResources", {

View File

@@ -122,14 +122,14 @@ export async function applyBlueprint({
) )
.limit(1); .limit(1);
if (site) { if (site && result.resource.mode === "port" && result.resource.protocol && result.resource.proxyPort && result.resource.destinationPort) {
logger.debug( logger.debug(
`Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}` `Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}`
); );
await addClientTargets( await addClientTargets(
site.newt.newtId, site.newt.newtId,
result.resource.destinationIp, result.resource.destination,
result.resource.destinationPort, result.resource.destinationPort,
result.resource.protocol, result.resource.protocol,
result.resource.proxyPort result.resource.proxyPort

View File

@@ -75,8 +75,9 @@ export async function updateClientResources(
.set({ .set({
name: resourceData.name || resourceNiceId, name: resourceData.name || resourceNiceId,
siteId: site.siteId, siteId: site.siteId,
mode: "port",
proxyPort: resourceData["proxy-port"]!, proxyPort: resourceData["proxy-port"]!,
destinationIp: resourceData.hostname, destination: resourceData.hostname,
destinationPort: resourceData["internal-port"], destinationPort: resourceData["internal-port"],
protocol: resourceData.protocol protocol: resourceData.protocol
}) })
@@ -98,8 +99,9 @@ export async function updateClientResources(
siteId: site.siteId, siteId: site.siteId,
niceId: resourceNiceId, niceId: resourceNiceId,
name: resourceData.name || resourceNiceId, name: resourceData.name || resourceNiceId,
mode: "port",
proxyPort: resourceData["proxy-port"]!, proxyPort: resourceData["proxy-port"]!,
destinationIp: resourceData.hostname, destination: resourceData.hostname,
destinationPort: resourceData["internal-port"], destinationPort: resourceData["internal-port"],
protocol: resourceData.protocol protocol: resourceData.protocol
}) })

View File

@@ -53,8 +53,6 @@ export async function verifyOrgAccess(
session: req.session session: req.session
}); });
logger.debug("Org check policy result", { policyCheck });
if (!policyCheck.allowed || policyCheck.error) { if (!policyCheck.allowed || policyCheck.error) {
return next( return next(
createHttpError( createHttpError(

View File

@@ -216,13 +216,18 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
const { tcpTargets, udpTargets } = allSiteResources.reduce( const { tcpTargets, udpTargets } = allSiteResources.reduce(
(acc, resource) => { (acc, resource) => {
// Only process port mode resources
if (resource.mode !== "port") {
return acc;
}
// Filter out invalid targets // Filter out invalid targets
if (!resource.proxyPort || !resource.destinationIp || !resource.destinationPort) { if (!resource.proxyPort || !resource.destination || !resource.destinationPort || !resource.protocol) {
return acc; return acc;
} }
// Format target into string // Format target into string
const formattedTarget = `${resource.proxyPort}:${resource.destinationIp}:${resource.destinationPort}`; const formattedTarget = `${resource.proxyPort}:${resource.destination}:${resource.destinationPort}`;
// Add to the appropriate protocol array // Add to the appropriate protocol array
if (resource.protocol === "tcp") { if (resource.protocol === "tcp") {

View File

@@ -22,13 +22,30 @@ const createSiteResourceParamsSchema = z
const createSiteResourceSchema = z const createSiteResourceSchema = z
.object({ .object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
protocol: z.enum(["tcp", "udp"]), mode: z.enum(["host", "cidr", "port"]),
proxyPort: z.number().int().positive(), protocol: z.enum(["tcp", "udp"]).optional(),
destinationPort: z.number().int().positive(), proxyPort: z.number().int().positive().optional(),
destinationIp: z.string(), destinationPort: z.number().int().positive().optional(),
enabled: z.boolean().default(true) destination: z.string().min(1),
}) enabled: z.boolean().default(true),
.strict(); alias: z.string().optional()
}).strict()
.refine(
(data) => {
if (data.mode === "port") {
return (
data.protocol !== undefined &&
data.proxyPort !== undefined &&
data.destinationPort !== undefined
);
}
return true;
},
{
message:
"Protocol, proxy port, and destination port are required for port mode"
}
);
export type CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>; export type CreateSiteResourceBody = z.infer<typeof createSiteResourceSchema>;
export type CreateSiteResourceResponse = SiteResource; export type CreateSiteResourceResponse = SiteResource;
@@ -82,11 +99,13 @@ export async function createSiteResource(
const { siteId, orgId } = parsedParams.data; const { siteId, orgId } = parsedParams.data;
const { const {
name, name,
mode,
protocol, protocol,
proxyPort, proxyPort,
destinationPort, destinationPort,
destinationIp, destination,
enabled enabled,
alias
} = parsedBody.data; } = parsedBody.data;
// Verify the site exists and belongs to the org // Verify the site exists and belongs to the org
@@ -100,26 +119,28 @@ export async function createSiteResource(
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
} }
// check if resource with same protocol and proxy port already exists // check if resource with same protocol and proxy port already exists (only for port mode)
const [existingResource] = await db if (mode === "port" && protocol && proxyPort) {
.select() const [existingResource] = await db
.from(siteResources) .select()
.where( .from(siteResources)
and( .where(
eq(siteResources.siteId, siteId), and(
eq(siteResources.orgId, orgId), eq(siteResources.siteId, siteId),
eq(siteResources.protocol, protocol), eq(siteResources.orgId, orgId),
eq(siteResources.proxyPort, proxyPort) eq(siteResources.protocol, protocol),
eq(siteResources.proxyPort, proxyPort)
)
) )
) .limit(1);
.limit(1); if (existingResource && existingResource.siteResourceId) {
if (existingResource && existingResource.siteResourceId) { return next(
return next( createHttpError(
createHttpError( HttpCode.CONFLICT,
HttpCode.CONFLICT, "A resource with the same protocol and proxy port already exists"
"A resource with the same protocol and proxy port already exists" )
) );
); }
} }
const niceId = await getUniqueSiteResourceName(orgId); const niceId = await getUniqueSiteResourceName(orgId);
@@ -132,11 +153,13 @@ export async function createSiteResource(
niceId, niceId,
orgId, orgId,
name, name,
protocol, mode,
proxyPort, protocol: mode === "port" ? protocol : null,
destinationPort, proxyPort: mode === "port" ? proxyPort : null,
destinationIp, destinationPort: mode === "port" ? destinationPort : null,
enabled destination,
enabled,
alias: alias || null
}) })
.returning(); .returning();
@@ -157,24 +180,29 @@ export async function createSiteResource(
siteResourceId: newSiteResource.siteResourceId siteResourceId: newSiteResource.siteResourceId
}); });
const [newt] = await db // Only add targets for port mode
.select() if (mode === "port" && protocol && proxyPort && destinationPort) {
.from(newts) const [newt] = await db
.where(eq(newts.siteId, site.siteId)) .select()
.limit(1); .from(newts)
.where(eq(newts.siteId, site.siteId))
.limit(1);
if (!newt) { if (!newt) {
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); return next(
createHttpError(HttpCode.NOT_FOUND, "Newt not found")
);
}
await addTargets(
newt.newtId,
destination,
destinationPort,
protocol,
proxyPort
);
} }
await addTargets(
newt.newtId,
destinationIp,
destinationPort,
protocol,
proxyPort
);
logger.info( logger.info(
`Created site resource ${newSiteResource.siteResourceId} for site ${siteId}` `Created site resource ${newSiteResource.siteResourceId} for site ${siteId}`
); );

View File

@@ -91,24 +91,27 @@ export async function deleteSiteResource(
eq(siteResources.orgId, orgId) eq(siteResources.orgId, orgId)
)); ));
const [newt] = await db // Only remove targets for port mode
.select() if (existingSiteResource.mode === "port" && existingSiteResource.protocol && existingSiteResource.proxyPort && existingSiteResource.destinationPort) {
.from(newts) const [newt] = await db
.where(eq(newts.siteId, site.siteId)) .select()
.limit(1); .from(newts)
.where(eq(newts.siteId, site.siteId))
.limit(1);
if (!newt) { if (!newt) {
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found"));
}
await removeTargets(
newt.newtId,
existingSiteResource.destination,
existingSiteResource.destinationPort,
existingSiteResource.protocol,
existingSiteResource.proxyPort
);
} }
await removeTargets(
newt.newtId,
existingSiteResource.destinationIp,
existingSiteResource.destinationPort,
existingSiteResource.protocol,
existingSiteResource.proxyPort
);
logger.info(`Deleted site resource ${siteResourceId} for site ${siteId}`); logger.info(`Deleted site resource ${siteResourceId} for site ${siteId}`);
return response(res, { return response(res, {

View File

@@ -32,7 +32,7 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({
}); });
export type ListAllSiteResourcesByOrgResponse = { export type ListAllSiteResourcesByOrgResponse = {
siteResources: (SiteResource & { siteName: string, siteNiceId: string })[]; siteResources: (SiteResource & { siteName: string, siteNiceId: string, siteAddress: string | null })[];
}; };
registry.registerPath({ registry.registerPath({
@@ -82,14 +82,18 @@ export async function listAllSiteResourcesByOrg(
siteResourceId: siteResources.siteResourceId, siteResourceId: siteResources.siteResourceId,
siteId: siteResources.siteId, siteId: siteResources.siteId,
orgId: siteResources.orgId, orgId: siteResources.orgId,
niceId: siteResources.niceId,
name: siteResources.name, name: siteResources.name,
mode: siteResources.mode,
protocol: siteResources.protocol, protocol: siteResources.protocol,
proxyPort: siteResources.proxyPort, proxyPort: siteResources.proxyPort,
destinationPort: siteResources.destinationPort, destinationPort: siteResources.destinationPort,
destinationIp: siteResources.destinationIp, destination: siteResources.destination,
enabled: siteResources.enabled, enabled: siteResources.enabled,
alias: siteResources.alias,
siteName: sites.name, siteName: sites.name,
siteNiceId: sites.niceId siteNiceId: sites.niceId,
siteAddress: sites.address
}) })
.from(siteResources) .from(siteResources)
.innerJoin(sites, eq(siteResources.siteId, sites.siteId)) .innerJoin(sites, eq(siteResources.siteId, sites.siteId))

View File

@@ -25,11 +25,13 @@ const updateSiteResourceParamsSchema = z
const updateSiteResourceSchema = z const updateSiteResourceSchema = z
.object({ .object({
name: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
protocol: z.enum(["tcp", "udp"]).optional(), mode: z.enum(["host", "cidr", "port"]).optional(),
proxyPort: z.number().int().positive().optional(), protocol: z.enum(["tcp", "udp"]).nullish(),
destinationPort: z.number().int().positive().optional(), proxyPort: z.number().int().positive().nullish(),
destinationIp: z.string().optional(), destinationPort: z.number().int().positive().nullish(),
enabled: z.boolean().optional() destination: z.string().min(1).optional(),
enabled: z.boolean().optional(),
alias: z.string().nullish()
}) })
.strict(); .strict();
@@ -114,39 +116,77 @@ export async function updateSiteResource(
); );
} }
const protocol = updateData.protocol || existingSiteResource.protocol; // Determine the final mode and validate port mode requirements
const proxyPort = const finalMode = updateData.mode || existingSiteResource.mode;
updateData.proxyPort || existingSiteResource.proxyPort; const finalProtocol = updateData.protocol !== undefined ? updateData.protocol : existingSiteResource.protocol;
const finalProxyPort = updateData.proxyPort !== undefined ? updateData.proxyPort : existingSiteResource.proxyPort;
const finalDestinationPort = updateData.destinationPort !== undefined ? updateData.destinationPort : existingSiteResource.destinationPort;
// check if resource with same protocol and proxy port already exists if (finalMode === "port") {
const [existingResource] = await db if (!finalProtocol || !finalProxyPort || !finalDestinationPort) {
.select() return next(
.from(siteResources) createHttpError(
.where( HttpCode.BAD_REQUEST,
and( "Protocol, proxy port, and destination port are required for port mode"
eq(siteResources.siteId, siteId), )
eq(siteResources.orgId, orgId), );
eq(siteResources.protocol, protocol), }
eq(siteResources.proxyPort, proxyPort)
// check if resource with same protocol and proxy port already exists
const [existingResource] = await db
.select()
.from(siteResources)
.where(
and(
eq(siteResources.siteId, siteId),
eq(siteResources.orgId, orgId),
eq(siteResources.protocol, finalProtocol),
eq(siteResources.proxyPort, finalProxyPort)
)
) )
) .limit(1);
.limit(1); if (
if ( existingResource &&
existingResource && existingResource.siteResourceId !== siteResourceId
existingResource.siteResourceId !== siteResourceId ) {
) { return next(
return next( createHttpError(
createHttpError( HttpCode.CONFLICT,
HttpCode.CONFLICT, "A resource with the same protocol and proxy port already exists"
"A resource with the same protocol and proxy port already exists" )
) );
); }
}
// Prepare update data
const updateValues: any = {};
if (updateData.name !== undefined) updateValues.name = updateData.name;
if (updateData.mode !== undefined) updateValues.mode = updateData.mode;
if (updateData.destination !== undefined) updateValues.destination = updateData.destination;
if (updateData.enabled !== undefined) updateValues.enabled = updateData.enabled;
// Handle nullish fields (can be undefined, null, or a value)
if (updateData.alias !== undefined) {
updateValues.alias = updateData.alias && updateData.alias.trim() ? updateData.alias : null;
}
// Handle port mode fields - include in update if explicitly provided (null or value) or if mode changed
const isModeChangingFromPort = existingSiteResource.mode === "port" && updateData.mode && updateData.mode !== "port";
if (updateData.protocol !== undefined || isModeChangingFromPort) {
updateValues.protocol = finalMode === "port" ? finalProtocol : null;
}
if (updateData.proxyPort !== undefined || isModeChangingFromPort) {
updateValues.proxyPort = finalMode === "port" ? finalProxyPort : null;
}
if (updateData.destinationPort !== undefined || isModeChangingFromPort) {
updateValues.destinationPort = finalMode === "port" ? finalDestinationPort : null;
} }
// Update the site resource // Update the site resource
const [updatedSiteResource] = await db const [updatedSiteResource] = await db
.update(siteResources) .update(siteResources)
.set(updateData) .set(updateValues)
.where( .where(
and( and(
eq(siteResources.siteResourceId, siteResourceId), eq(siteResources.siteResourceId, siteResourceId),
@@ -156,24 +196,27 @@ export async function updateSiteResource(
) )
.returning(); .returning();
const [newt] = await db // Only add targets for port mode
.select() if (updatedSiteResource.mode === "port" && updatedSiteResource.protocol && updatedSiteResource.proxyPort && updatedSiteResource.destinationPort) {
.from(newts) const [newt] = await db
.where(eq(newts.siteId, site.siteId)) .select()
.limit(1); .from(newts)
.where(eq(newts.siteId, site.siteId))
.limit(1);
if (!newt) { if (!newt) {
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found"));
}
await addTargets(
newt.newtId,
updatedSiteResource.destination,
updatedSiteResource.destinationPort,
updatedSiteResource.protocol,
updatedSiteResource.proxyPort
);
} }
await addTargets(
newt.newtId,
updatedSiteResource.destinationIp,
updatedSiteResource.destinationPort,
updatedSiteResource.protocol,
updatedSiteResource.proxyPort
);
logger.info( logger.info(
`Updated site resource ${siteResourceId} for site ${siteId}` `Updated site resource ${siteResourceId} for site ${siteId}`
); );

View File

@@ -103,11 +103,14 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
name: siteResource.name, name: siteResource.name,
orgId: params.orgId, orgId: params.orgId,
siteName: siteResource.siteName, siteName: siteResource.siteName,
siteAddress: siteResource.siteAddress || null,
mode: siteResource.mode || "port" as any,
protocol: siteResource.protocol, protocol: siteResource.protocol,
proxyPort: siteResource.proxyPort, proxyPort: siteResource.proxyPort,
siteId: siteResource.siteId, siteId: siteResource.siteId,
destinationIp: siteResource.destinationIp, destination: siteResource.destination,
destinationPort: siteResource.destinationPort, destinationPort: siteResource.destinationPort,
alias: siteResource.alias || null,
siteNiceId: siteResource.siteNiceId siteNiceId: siteResource.siteNiceId
}; };
} }

View File

@@ -86,20 +86,24 @@ export default function CreateInternalResourceDialog({
.min(1, t("createInternalResourceDialogNameRequired")) .min(1, t("createInternalResourceDialogNameRequired"))
.max(255, t("createInternalResourceDialogNameMaxLength")), .max(255, t("createInternalResourceDialogNameMaxLength")),
siteId: z.number().int().positive(t("createInternalResourceDialogPleaseSelectSite")), siteId: z.number().int().positive(t("createInternalResourceDialogPleaseSelectSite")),
protocol: z.enum(["tcp", "udp"]), mode: z.enum(["host", "cidr", "port"]),
protocol: z.enum(["tcp", "udp"]).nullish(),
proxyPort: z proxyPort: z
.number() .number()
.int() .int()
.positive() .positive()
.min(1, t("createInternalResourceDialogProxyPortMin")) .min(1, t("createInternalResourceDialogProxyPortMin"))
.max(65535, t("createInternalResourceDialogProxyPortMax")), .max(65535, t("createInternalResourceDialogProxyPortMax"))
destinationIp: z.string(), .nullish(),
destination: z.string().min(1),
destinationPort: z destinationPort: z
.number() .number()
.int() .int()
.positive() .positive()
.min(1, t("createInternalResourceDialogDestinationPortMin")) .min(1, t("createInternalResourceDialogDestinationPortMin"))
.max(65535, t("createInternalResourceDialogDestinationPortMax")), .max(65535, t("createInternalResourceDialogDestinationPortMax"))
.nullish(),
alias: z.string().nullish(),
roles: z.array( roles: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
@@ -112,8 +116,44 @@ export default function CreateInternalResourceDialog({
text: z.string() text: z.string()
}) })
).optional() ).optional()
}); })
.refine(
(data) => {
if (data.mode === "port") {
return data.protocol !== undefined && data.protocol !== null;
}
return true;
},
{
message: t("createInternalResourceDialogProtocol") + " is required for port mode",
path: ["protocol"]
}
)
.refine(
(data) => {
if (data.mode === "port") {
return data.proxyPort !== undefined && data.proxyPort !== null;
}
return true;
},
{
message: t("createInternalResourceDialogSitePort") + " is required for port mode",
path: ["proxyPort"]
}
)
.refine(
(data) => {
if (data.mode === "port") {
return data.destinationPort !== undefined && data.destinationPort !== null;
}
return true;
},
{
message: t("targetPort") + " is required for port mode",
path: ["destinationPort"]
}
);
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>([]); const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>([]);
@@ -130,24 +170,30 @@ export default function CreateInternalResourceDialog({
defaultValues: { defaultValues: {
name: "", name: "",
siteId: availableSites[0]?.siteId || 0, siteId: availableSites[0]?.siteId || 0,
mode: "host",
protocol: "tcp", protocol: "tcp",
proxyPort: undefined, proxyPort: undefined,
destinationIp: "", destination: "",
destinationPort: undefined, destinationPort: undefined,
alias: "",
roles: [], roles: [],
users: [] users: []
} }
}); });
const mode = form.watch("mode");
useEffect(() => { useEffect(() => {
if (open && availableSites.length > 0) { if (open && availableSites.length > 0) {
form.reset({ form.reset({
name: "", name: "",
siteId: availableSites[0].siteId, siteId: availableSites[0].siteId,
mode: "host",
protocol: "tcp", protocol: "tcp",
proxyPort: undefined, proxyPort: undefined,
destinationIp: "", destination: "",
destinationPort: undefined, destinationPort: undefined,
alias: "",
roles: [], roles: [],
users: [] users: []
}); });
@@ -194,11 +240,13 @@ export default function CreateInternalResourceDialog({
`/org/${orgId}/site/${data.siteId}/resource`, `/org/${orgId}/site/${data.siteId}/resource`,
{ {
name: data.name, name: data.name,
protocol: data.protocol, mode: data.mode,
proxyPort: data.proxyPort, protocol: data.mode === "port" ? data.protocol : undefined,
destinationIp: data.destinationIp, proxyPort: data.mode === "port" ? data.proxyPort : undefined,
destinationPort: data.destinationPort, destinationPort: data.mode === "port" ? data.destinationPort : undefined,
enabled: true destination: data.destination,
enabled: true,
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined
} }
); );
@@ -294,126 +342,151 @@ export default function CreateInternalResourceDialog({
)} )}
/> />
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>{t("createInternalResourceDialogSite")}</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between",
!field.value && "text-muted-foreground"
)}
>
{field.value
? availableSites.find(
(site) => site.siteId === field.value
)?.name
: t("createInternalResourceDialogSelectSite")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder={t("createInternalResourceDialogSearchSites")} />
<CommandList>
<CommandEmpty>{t("createInternalResourceDialogNoSitesFound")}</CommandEmpty>
<CommandGroup>
{availableSites.map((site) => (
<CommandItem
key={site.siteId}
value={site.name}
onSelect={() => {
field.onChange(site.siteId);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
field.value === site.siteId
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("createInternalResourceDialogProtocol")}
</FormLabel>
<Select
onValueChange={
field.onChange
}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">
{t("createInternalResourceDialogTcp")}
</SelectItem>
<SelectItem value="udp">
{t("createInternalResourceDialogUdp")}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="proxyPort" name="siteId"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="flex flex-col">
<FormLabel>{t("createInternalResourceDialogSitePort")}</FormLabel> <FormLabel>{t("createInternalResourceDialogSite")}</FormLabel>
<FormControl> <Popover>
<Input <PopoverTrigger asChild>
type="number" <FormControl>
value={field.value || ""} <Button
onChange={(e) => variant="outline"
field.onChange( role="combobox"
e.target.value === "" ? undefined : parseInt(e.target.value) className={cn(
) "w-full justify-between",
} !field.value && "text-muted-foreground"
/> )}
</FormControl> >
<FormDescription> {field.value
{t("createInternalResourceDialogSitePortDescription")} ? availableSites.find(
</FormDescription> (site) => site.siteId === field.value
)?.name
: t("createInternalResourceDialogSelectSite")}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder={t("createInternalResourceDialogSearchSites")} />
<CommandList>
<CommandEmpty>{t("createInternalResourceDialogNoSitesFound")}</CommandEmpty>
<CommandGroup>
{availableSites.map((site) => (
<CommandItem
key={site.siteId}
value={site.name}
onSelect={() => {
field.onChange(site.siteId);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
field.value === site.siteId
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="mode"
render={({ field }) => (
<FormItem>
<FormLabel>{t("createInternalResourceDialogMode")}</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="port">{t("createInternalResourceDialogModePort")}</SelectItem>
<SelectItem value="host">{t("createInternalResourceDialogModeHost")}</SelectItem>
<SelectItem value="cidr">{t("createInternalResourceDialogModeCidr")}</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{mode === "port" && (
<>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("createInternalResourceDialogProtocol")}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value ?? undefined}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">
{t("createInternalResourceDialogTcp")}
</SelectItem>
<SelectItem value="udp">
{t("createInternalResourceDialogUdp")}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>{t("createInternalResourceDialogSitePort")}</FormLabel>
<FormControl>
<Input
type="number"
value={field.value || ""}
onChange={(e) =>
field.onChange(
e.target.value === "" ? undefined : parseInt(e.target.value)
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</>
)}
</div> </div>
</div> </div>
@@ -423,28 +496,28 @@ export default function CreateInternalResourceDialog({
{t("createInternalResourceDialogTargetConfiguration")} {t("createInternalResourceDialogTargetConfiguration")}
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <FormField
<FormField control={form.control}
control={form.control} name="destination"
name="destinationIp" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>
<FormLabel> {t("createInternalResourceDialogDestination")}
{t("targetAddr")} </FormLabel>
</FormLabel> <FormControl>
<FormControl> <Input {...field} />
<Input </FormControl>
{...field} <FormDescription>
/> {mode === "host" && t("createInternalResourceDialogDestinationHostDescription")}
</FormControl> {mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")}
<FormDescription> {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")}
{t("createInternalResourceDialogDestinationIPDescription")} </FormDescription>
</FormDescription> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> )}
)} />
/>
{mode === "port" && (
<FormField <FormField
control={form.control} control={form.control}
name="destinationPort" name="destinationPort"
@@ -471,12 +544,33 @@ export default function CreateInternalResourceDialog({
</FormItem> </FormItem>
)} )}
/> />
</div> )}
</div> </div>
</div> </div>
{/* Alias */}
{mode !== "cidr" && (
<div>
<FormField
control={form.control}
name="alias"
render={({ field }) => (
<FormItem>
<FormLabel>{t("createInternalResourceDialogAlias")}</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ""} />
</FormControl>
<FormDescription>
{t("createInternalResourceDialogAliasDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{/* Access Control Section */} {/* Access Control Section */}
<Separator />
<div> <div>
<h3 className="text-lg font-semibold mb-4"> <h3 className="text-lg font-semibold mb-4">
{t("resourceUsersRoles")} {t("resourceUsersRoles")}

View File

@@ -50,11 +50,13 @@ type InternalResourceData = {
name: string; name: string;
orgId: string; orgId: string;
siteName: string; siteName: string;
protocol: string; mode: "host" | "cidr" | "port";
protocol: string | null;
proxyPort: number | null; proxyPort: number | null;
siteId: number; siteId: number;
destinationIp?: string; destination: string;
destinationPort?: number; destinationPort?: number | null;
alias?: string | null;
}; };
type EditInternalResourceDialogProps = { type EditInternalResourceDialogProps = {
@@ -78,10 +80,12 @@ export default function EditInternalResourceDialog({
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")),
protocol: z.enum(["tcp", "udp"]), mode: z.enum(["host", "cidr", "port"]),
proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")), protocol: z.enum(["tcp", "udp"]).nullish(),
destinationIp: z.string(), proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(),
destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")), destination: z.string().min(1),
destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(),
alias: z.string().nullish(),
roles: z.array( roles: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
@@ -94,7 +98,43 @@ export default function EditInternalResourceDialog({
text: z.string() text: z.string()
}) })
).optional() ).optional()
}); })
.refine(
(data) => {
if (data.mode === "port") {
return data.protocol !== undefined && data.protocol !== null;
}
return true;
},
{
message: t("editInternalResourceDialogProtocol") + " is required for port mode",
path: ["protocol"]
}
)
.refine(
(data) => {
if (data.mode === "port") {
return data.proxyPort !== undefined && data.proxyPort !== null;
}
return true;
},
{
message: t("editInternalResourceDialogSitePort") + " is required for port mode",
path: ["proxyPort"]
}
)
.refine(
(data) => {
if (data.mode === "port") {
return data.destinationPort !== undefined && data.destinationPort !== null;
}
return true;
},
{
message: t("targetPort") + " is required for port mode",
path: ["destinationPort"]
}
);
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
@@ -108,15 +148,19 @@ export default function EditInternalResourceDialog({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
name: resource.name, name: resource.name,
protocol: resource.protocol as "tcp" | "udp", mode: resource.mode || "host",
proxyPort: resource.proxyPort || undefined, protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
destinationIp: resource.destinationIp || "", proxyPort: resource.proxyPort ?? undefined,
destinationPort: resource.destinationPort || undefined, destination: resource.destination || "",
destinationPort: resource.destinationPort ?? undefined,
alias: resource.alias ?? null,
roles: [], roles: [],
users: [] users: []
} }
}); });
const mode = form.watch("mode");
const fetchRolesAndUsers = async () => { const fetchRolesAndUsers = async () => {
setLoadingRolesUsers(true); setLoadingRolesUsers(true);
try { try {
@@ -180,10 +224,12 @@ export default function EditInternalResourceDialog({
if (open) { if (open) {
form.reset({ form.reset({
name: resource.name, name: resource.name,
protocol: resource.protocol as "tcp" | "udp", mode: resource.mode || "host",
proxyPort: resource.proxyPort || undefined, protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
destinationIp: resource.destinationIp || "", proxyPort: resource.proxyPort ?? undefined,
destinationPort: resource.destinationPort || undefined, destination: resource.destination || "",
destinationPort: resource.destinationPort ?? undefined,
alias: resource.alias ?? null,
roles: [], roles: [],
users: [] users: []
}); });
@@ -198,10 +244,12 @@ export default function EditInternalResourceDialog({
// Update the site resource // Update the site resource
await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, { await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, {
name: data.name, name: data.name,
protocol: data.protocol, mode: data.mode,
proxyPort: data.proxyPort, protocol: data.mode === "port" ? data.protocol : null,
destinationIp: data.destinationIp, proxyPort: data.mode === "port" ? data.proxyPort : null,
destinationPort: data.destinationPort destinationPort: data.mode === "port" ? data.destinationPort : null,
destination: data.destination,
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : null
}); });
// Update roles and users // Update roles and users
@@ -264,50 +312,78 @@ export default function EditInternalResourceDialog({
)} )}
/> />
<div className="grid grid-cols-2 gap-4"> <FormField
<FormField control={form.control}
control={form.control} name="mode"
name="protocol" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>{t("editInternalResourceDialogMode")}</FormLabel>
<FormLabel>{t("editInternalResourceDialogProtocol")}</FormLabel> <Select
<Select onValueChange={field.onChange}
onValueChange={field.onChange} value={field.value}
value={field.value} >
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">TCP</SelectItem>
<SelectItem value="udp">UDP</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>{t("editInternalResourceDialogSitePort")}</FormLabel>
<FormControl> <FormControl>
<Input <SelectTrigger>
type="number" <SelectValue />
{...field} </SelectTrigger>
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
/>
</FormControl> </FormControl>
<FormMessage /> <SelectContent>
</FormItem> <SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem>
)} <SelectItem value="host">{t("editInternalResourceDialogModeHost")}</SelectItem>
/> <SelectItem value="cidr">{t("editInternalResourceDialogModeCidr")}</SelectItem>
</div> </SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{mode === "port" && (
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>{t("editInternalResourceDialogProtocol")}</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value ?? undefined}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">TCP</SelectItem>
<SelectItem value="udp">UDP</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>{t("editInternalResourceDialogSitePort")}</FormLabel>
<FormControl>
<Input
type="number"
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</div> </div>
</div> </div>
@@ -315,21 +391,26 @@ export default function EditInternalResourceDialog({
<div> <div>
<h3 className="text-lg font-semibold mb-4">{t("editInternalResourceDialogTargetConfiguration")}</h3> <h3 className="text-lg font-semibold mb-4">{t("editInternalResourceDialogTargetConfiguration")}</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <FormField
<FormField control={form.control}
control={form.control} name="destination"
name="destinationIp" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>{t("editInternalResourceDialogDestination")}</FormLabel>
<FormLabel>{t("targetAddr")}</FormLabel> <FormControl>
<FormControl> <Input {...field} />
<Input {...field} /> </FormControl>
</FormControl> <FormDescription>
<FormMessage /> {mode === "host" && t("editInternalResourceDialogDestinationHostDescription")}
</FormItem> {mode === "cidr" && t("editInternalResourceDialogDestinationCidrDescription")}
)} {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")}
/> </FormDescription>
<FormMessage />
</FormItem>
)}
/>
{mode === "port" && (
<FormField <FormField
control={form.control} control={form.control}
name="destinationPort" name="destinationPort"
@@ -339,20 +420,41 @@ export default function EditInternalResourceDialog({
<FormControl> <FormControl>
<Input <Input
type="number" type="number"
{...field} value={field.value || ""}
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)} onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div> )}
</div> </div>
</div> </div>
{/* Alias */}
{mode !== "cidr" && (
<div>
<FormField
control={form.control}
name="alias"
render={({ field }) => (
<FormItem>
<FormLabel>{t("editInternalResourceDialogAlias")}</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ""} />
</FormControl>
<FormDescription>
{t("editInternalResourceDialogAliasDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{/* Access Control Section */} {/* Access Control Section */}
<Separator />
<div> <div>
<h3 className="text-lg font-semibold mb-4"> <h3 className="text-lg font-semibold mb-4">
{t("resourceUsersRoles")} {t("resourceUsersRoles")}

View File

@@ -90,12 +90,15 @@ export type InternalResourceRow = {
name: string; name: string;
orgId: string; orgId: string;
siteName: string; siteName: string;
protocol: string; siteAddress: string | null;
mode: "host" | "cidr" | "port";
protocol: string | null;
proxyPort: number | null; proxyPort: number | null;
siteId: number; siteId: number;
siteNiceId: string; siteNiceId: string;
destinationIp: string; destination: string;
destinationPort: number; destinationPort: number | null;
alias: string | null;
}; };
type Site = ListSitesResponse["sites"][0]; type Site = ListSitesResponse["sites"][0];
@@ -571,24 +574,16 @@ export default function ResourcesTable({
} }
}, },
{ {
accessorKey: "protocol", accessorKey: "mode",
header: () => (<span className="p-3">{t("protocol")}</span>), header: () => (<span className="p-3">{t("editInternalResourceDialogMode")}</span>),
cell: ({ row }) => { cell: ({ row }) => {
const resourceRow = row.original; const resourceRow = row.original;
return <span>{resourceRow.protocol.toUpperCase()}</span>; const modeLabels: Record<"host" | "cidr" | "port", string> = {
} host: t("editInternalResourceDialogModeHost"),
}, cidr: t("editInternalResourceDialogModeCidr"),
{ port: t("editInternalResourceDialogModePort")
accessorKey: "proxyPort", };
header: () => (<span className="p-3">{t("proxyPort")}</span>), return <span>{modeLabels[resourceRow.mode]}</span>;
cell: ({ row }) => {
const resourceRow = row.original;
return (
<CopyToClipboard
text={resourceRow.proxyPort?.toString() || ""}
isLink={false}
/>
);
} }
}, },
{ {
@@ -596,8 +591,35 @@ export default function ResourcesTable({
header: () => (<span className="p-3">{t("resourcesTableDestination")}</span>), header: () => (<span className="p-3">{t("resourcesTableDestination")}</span>),
cell: ({ row }) => { cell: ({ row }) => {
const resourceRow = row.original; const resourceRow = row.original;
const destination = `${resourceRow.destinationIp}:${resourceRow.destinationPort}`; let displayText: string;
return <CopyToClipboard text={destination} isLink={false} />; let copyText: string;
if (resourceRow.mode === "port" && resourceRow.protocol && resourceRow.proxyPort && resourceRow.destinationPort) {
const protocol = resourceRow.protocol.toUpperCase();
// For port mode: site part uses alias or site address, destination part uses destination IP
// If site address has CIDR notation, extract just the IP address
let siteAddress = resourceRow.siteAddress;
if (siteAddress && siteAddress.includes("/")) {
siteAddress = siteAddress.split("/")[0];
}
const siteDisplay = resourceRow.alias || siteAddress;
displayText = `${protocol} ${siteDisplay}:${resourceRow.proxyPort} -> ${resourceRow.destination}:${resourceRow.destinationPort}`;
copyText = `${siteDisplay}:${resourceRow.proxyPort}`;
} else if (resourceRow.mode === "host") {
// For host mode: use alias if available, otherwise use destination
const destinationDisplay = resourceRow.alias || resourceRow.destination;
displayText = destinationDisplay;
copyText = destinationDisplay;
} else if (resourceRow.mode === "cidr") {
displayText = resourceRow.destination;
copyText = resourceRow.destination;
} else {
const destinationDisplay = resourceRow.alias || resourceRow.destination;
displayText = destinationDisplay;
copyText = destinationDisplay;
}
return <CopyToClipboard text={copyText} isLink={false} displayText={displayText} />;
} }
}, },

View File

@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-sm duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:scale-95 data-[state=open]:scale-100 sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:scale-95 data-[state=open]:scale-100 sm:rounded-lg",
className className
)} )}
{...props} {...props}