mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-11 21:41:41 +02:00
add sites to resource launcher panel
This commit is contained in:
@@ -4276,6 +4276,9 @@
|
|||||||
"resourceLauncherViewAsAdmin": "View as Admin",
|
"resourceLauncherViewAsAdmin": "View as Admin",
|
||||||
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
|
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
|
||||||
"resourceLauncherResourceDetails": "Resource Details",
|
"resourceLauncherResourceDetails": "Resource Details",
|
||||||
|
"resourceLauncherSitesDescription": "The resource is accessible via the following sites.",
|
||||||
|
"resourceLauncherViewSiteAsAdmin": "View Site as Admin",
|
||||||
|
"resourceLauncherFilterBySite": "Filter by Site",
|
||||||
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
|
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
|
||||||
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
|
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
|
||||||
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
|
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
|
||||||
|
|||||||
@@ -522,6 +522,7 @@ async function fetchLabelsForResources(
|
|||||||
type SiteGroupRow = {
|
type SiteGroupRow = {
|
||||||
siteId: number;
|
siteId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
niceId: string;
|
||||||
type: string;
|
type: string;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
@@ -556,6 +557,7 @@ async function listSiteGroups(
|
|||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
name: sites.name,
|
name: sites.name,
|
||||||
|
niceId: sites.niceId,
|
||||||
type: sites.type,
|
type: sites.type,
|
||||||
online: sites.online,
|
online: sites.online,
|
||||||
itemCount: countDistinct(resources.resourceId)
|
itemCount: countDistinct(resources.resourceId)
|
||||||
@@ -576,7 +578,13 @@ async function listSiteGroups(
|
|||||||
|
|
||||||
const publicRows = await publicQuery
|
const publicRows = await publicQuery
|
||||||
.where(and(...publicConditions))
|
.where(and(...publicConditions))
|
||||||
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
.groupBy(
|
||||||
|
sites.siteId,
|
||||||
|
sites.name,
|
||||||
|
sites.niceId,
|
||||||
|
sites.type,
|
||||||
|
sites.online
|
||||||
|
);
|
||||||
|
|
||||||
for (const row of publicRows) {
|
for (const row of publicRows) {
|
||||||
const existing = siteCountMap.get(row.siteId);
|
const existing = siteCountMap.get(row.siteId);
|
||||||
@@ -586,6 +594,7 @@ async function listSiteGroups(
|
|||||||
siteCountMap.set(row.siteId, {
|
siteCountMap.set(row.siteId, {
|
||||||
siteId: row.siteId,
|
siteId: row.siteId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
niceId: row.niceId,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
online: row.online,
|
online: row.online,
|
||||||
itemCount: Number(row.itemCount)
|
itemCount: Number(row.itemCount)
|
||||||
@@ -612,6 +621,7 @@ async function listSiteGroups(
|
|||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
name: sites.name,
|
name: sites.name,
|
||||||
|
niceId: sites.niceId,
|
||||||
type: sites.type,
|
type: sites.type,
|
||||||
online: sites.online,
|
online: sites.online,
|
||||||
itemCount: countDistinct(siteResources.siteResourceId)
|
itemCount: countDistinct(siteResources.siteResourceId)
|
||||||
@@ -638,7 +648,13 @@ async function listSiteGroups(
|
|||||||
|
|
||||||
const siteRows = await siteResourceQuery
|
const siteRows = await siteResourceQuery
|
||||||
.where(and(...siteConditions))
|
.where(and(...siteConditions))
|
||||||
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
.groupBy(
|
||||||
|
sites.siteId,
|
||||||
|
sites.name,
|
||||||
|
sites.niceId,
|
||||||
|
sites.type,
|
||||||
|
sites.online
|
||||||
|
);
|
||||||
|
|
||||||
for (const row of siteRows) {
|
for (const row of siteRows) {
|
||||||
const existing = siteCountMap.get(row.siteId);
|
const existing = siteCountMap.get(row.siteId);
|
||||||
@@ -648,6 +664,7 @@ async function listSiteGroups(
|
|||||||
siteCountMap.set(row.siteId, {
|
siteCountMap.set(row.siteId, {
|
||||||
siteId: row.siteId,
|
siteId: row.siteId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
niceId: row.niceId,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
online: row.online,
|
online: row.online,
|
||||||
itemCount: Number(row.itemCount)
|
itemCount: Number(row.itemCount)
|
||||||
@@ -1061,6 +1078,43 @@ export async function listLauncherGroupsForUser(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toLauncherSiteInfo(row: {
|
||||||
|
siteId: number | null;
|
||||||
|
siteName: string | null;
|
||||||
|
siteNiceId: string | null;
|
||||||
|
siteType: string | null;
|
||||||
|
siteOnline: boolean | null;
|
||||||
|
}): LauncherSiteInfo | null {
|
||||||
|
if (
|
||||||
|
row.siteId == null ||
|
||||||
|
row.siteName == null ||
|
||||||
|
row.siteNiceId == null ||
|
||||||
|
row.siteType == null
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
siteId: row.siteId,
|
||||||
|
name: row.siteName,
|
||||||
|
niceId: row.siteNiceId,
|
||||||
|
type: row.siteType,
|
||||||
|
online: row.siteOnline ?? undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickPrimarySite(
|
||||||
|
sites: LauncherSiteInfo[],
|
||||||
|
siteIdFilter?: number
|
||||||
|
): LauncherSiteInfo | undefined {
|
||||||
|
if (sites.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (siteIdFilter != null) {
|
||||||
|
return sites.find((site) => site.siteId === siteIdFilter) ?? sites[0];
|
||||||
|
}
|
||||||
|
return sites[0];
|
||||||
|
}
|
||||||
|
|
||||||
async function mapPublicResources(
|
async function mapPublicResources(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
resourceIds: number[],
|
resourceIds: number[],
|
||||||
@@ -1084,6 +1138,7 @@ async function mapPublicResources(
|
|||||||
enabled: resources.enabled,
|
enabled: resources.enabled,
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
siteName: sites.name,
|
siteName: sites.name,
|
||||||
|
siteNiceId: sites.niceId,
|
||||||
siteType: sites.type,
|
siteType: sites.type,
|
||||||
siteOnline: sites.online,
|
siteOnline: sites.online,
|
||||||
exitNodeEndpoint: exitNodes.endpoint
|
exitNodeEndpoint: exitNodes.endpoint
|
||||||
@@ -1097,56 +1152,65 @@ async function mapPublicResources(
|
|||||||
inArray(resources.resourceId, resourceIds),
|
inArray(resources.resourceId, resourceIds),
|
||||||
eq(resources.orgId, orgId),
|
eq(resources.orgId, orgId),
|
||||||
eq(resources.enabled, true),
|
eq(resources.enabled, true),
|
||||||
eq(resources.status, "approved"),
|
eq(resources.status, "approved")
|
||||||
siteIdFilter != null
|
|
||||||
? eq(sites.siteId, siteIdFilter)
|
|
||||||
: undefined
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const seen = new Set<string>();
|
const byKey = new Map<string, LauncherResource>();
|
||||||
const result: LauncherResource[] = [];
|
const siteIdsByKey = new Map<string, Set<number>>();
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const key = `public:${row.resourceId}`;
|
const key = `public:${row.resourceId}`;
|
||||||
if (seen.has(key)) {
|
let item = byKey.get(key);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
const access = formatPublicResourceAccess({
|
||||||
|
mode: row.mode,
|
||||||
|
fullDomain: row.fullDomain,
|
||||||
|
ssl: row.ssl,
|
||||||
|
proxyPort: row.proxyPort,
|
||||||
|
wildcard: row.wildcard,
|
||||||
|
exitNodeEndpoint: row.exitNodeEndpoint
|
||||||
|
});
|
||||||
|
|
||||||
|
item = {
|
||||||
|
launcherResourceKey: key,
|
||||||
|
resourceType: "public",
|
||||||
|
resourceId: row.resourceId,
|
||||||
|
niceId: row.niceId,
|
||||||
|
name: row.name,
|
||||||
|
...access,
|
||||||
|
iconUrl: null,
|
||||||
|
enabled: row.enabled,
|
||||||
|
mode: row.mode,
|
||||||
|
labels: labelMaps.byResourceId.get(row.resourceId) ?? [],
|
||||||
|
sites: []
|
||||||
|
};
|
||||||
|
byKey.set(key, item);
|
||||||
|
siteIdsByKey.set(key, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
const site = toLauncherSiteInfo(row);
|
||||||
|
if (!site) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
seen.add(key);
|
|
||||||
|
|
||||||
const access = formatPublicResourceAccess({
|
const seenSiteIds = siteIdsByKey.get(key)!;
|
||||||
mode: row.mode,
|
if (seenSiteIds.has(site.siteId)) {
|
||||||
fullDomain: row.fullDomain,
|
continue;
|
||||||
ssl: row.ssl,
|
}
|
||||||
proxyPort: row.proxyPort,
|
seenSiteIds.add(site.siteId);
|
||||||
wildcard: row.wildcard,
|
item.sites.push(site);
|
||||||
exitNodeEndpoint: row.exitNodeEndpoint
|
|
||||||
});
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
launcherResourceKey: key,
|
|
||||||
resourceType: "public",
|
|
||||||
resourceId: row.resourceId,
|
|
||||||
niceId: row.niceId,
|
|
||||||
name: row.name,
|
|
||||||
...access,
|
|
||||||
iconUrl: null,
|
|
||||||
enabled: row.enabled,
|
|
||||||
mode: row.mode,
|
|
||||||
labels: labelMaps.byResourceId.get(row.resourceId) ?? [],
|
|
||||||
site:
|
|
||||||
row.siteId != null
|
|
||||||
? {
|
|
||||||
siteId: row.siteId,
|
|
||||||
name: row.siteName!,
|
|
||||||
type: row.siteType!,
|
|
||||||
online: row.siteOnline ?? undefined
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
for (const item of byKey.values()) {
|
||||||
|
item.sites.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
||||||
|
);
|
||||||
|
item.site = pickPrimarySite(item.sites, siteIdFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(byKey.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mapSiteResources(
|
async function mapSiteResources(
|
||||||
@@ -1175,6 +1239,7 @@ async function mapSiteResources(
|
|||||||
enabled: siteResources.enabled,
|
enabled: siteResources.enabled,
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
siteName: sites.name,
|
siteName: sites.name,
|
||||||
|
siteNiceId: sites.niceId,
|
||||||
siteType: sites.type,
|
siteType: sites.type,
|
||||||
siteOnline: sites.online
|
siteOnline: sites.online
|
||||||
})
|
})
|
||||||
@@ -1189,59 +1254,69 @@ async function mapSiteResources(
|
|||||||
inArray(siteResources.siteResourceId, siteResourceIds),
|
inArray(siteResources.siteResourceId, siteResourceIds),
|
||||||
eq(siteResources.orgId, orgId),
|
eq(siteResources.orgId, orgId),
|
||||||
eq(siteResources.enabled, true),
|
eq(siteResources.enabled, true),
|
||||||
eq(siteResources.status, "approved"),
|
eq(siteResources.status, "approved")
|
||||||
siteIdFilter != null
|
|
||||||
? eq(sites.siteId, siteIdFilter)
|
|
||||||
: undefined
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const seen = new Set<string>();
|
const byKey = new Map<string, LauncherResource>();
|
||||||
const result: LauncherResource[] = [];
|
const siteIdsByKey = new Map<string, Set<number>>();
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const key = `site:${row.siteResourceId}`;
|
const key = `site:${row.siteResourceId}`;
|
||||||
if (seen.has(key)) {
|
let item = byKey.get(key);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
const access = formatSiteResourceAccess({
|
||||||
|
mode: row.mode,
|
||||||
|
destination: row.destination,
|
||||||
|
destinationPort: row.destinationPort,
|
||||||
|
scheme: row.scheme,
|
||||||
|
ssl: row.ssl,
|
||||||
|
fullDomain: row.fullDomain,
|
||||||
|
alias: row.alias,
|
||||||
|
aliasAddress: row.aliasAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
item = {
|
||||||
|
launcherResourceKey: key,
|
||||||
|
resourceType: "site",
|
||||||
|
resourceId: row.siteResourceId,
|
||||||
|
siteResourceId: row.siteResourceId,
|
||||||
|
niceId: row.niceId,
|
||||||
|
name: row.name,
|
||||||
|
...access,
|
||||||
|
iconUrl: null,
|
||||||
|
enabled: row.enabled,
|
||||||
|
mode: row.mode,
|
||||||
|
labels:
|
||||||
|
labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [],
|
||||||
|
sites: []
|
||||||
|
};
|
||||||
|
byKey.set(key, item);
|
||||||
|
siteIdsByKey.set(key, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
const site = toLauncherSiteInfo(row);
|
||||||
|
if (!site) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
seen.add(key);
|
|
||||||
|
|
||||||
const access = formatSiteResourceAccess({
|
const seenSiteIds = siteIdsByKey.get(key)!;
|
||||||
mode: row.mode,
|
if (seenSiteIds.has(site.siteId)) {
|
||||||
destination: row.destination,
|
continue;
|
||||||
destinationPort: row.destinationPort,
|
}
|
||||||
scheme: row.scheme,
|
seenSiteIds.add(site.siteId);
|
||||||
ssl: row.ssl,
|
item.sites.push(site);
|
||||||
fullDomain: row.fullDomain,
|
|
||||||
alias: row.alias,
|
|
||||||
aliasAddress: row.aliasAddress
|
|
||||||
});
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
launcherResourceKey: key,
|
|
||||||
resourceType: "site",
|
|
||||||
resourceId: row.siteResourceId,
|
|
||||||
siteResourceId: row.siteResourceId,
|
|
||||||
niceId: row.niceId,
|
|
||||||
name: row.name,
|
|
||||||
...access,
|
|
||||||
iconUrl: null,
|
|
||||||
enabled: row.enabled,
|
|
||||||
mode: row.mode,
|
|
||||||
labels: labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [],
|
|
||||||
site:
|
|
||||||
row.siteId != null
|
|
||||||
? {
|
|
||||||
siteId: row.siteId,
|
|
||||||
name: row.siteName!,
|
|
||||||
type: row.siteType!,
|
|
||||||
online: row.siteOnline ?? undefined
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
for (const item of byKey.values()) {
|
||||||
|
item.sites.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
||||||
|
);
|
||||||
|
item.site = pickPrimarySite(item.sites, siteIdFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(byKey.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterResourcesBySite(
|
function filterResourcesBySite(
|
||||||
@@ -1252,13 +1327,17 @@ function filterResourcesBySite(
|
|||||||
return items.filter((item) => item.mode === "inference");
|
return items.filter((item) => item.mode === "inference");
|
||||||
}
|
}
|
||||||
if (groupKey === LAUNCHER_NO_SITE_GROUP_KEY) {
|
if (groupKey === LAUNCHER_NO_SITE_GROUP_KEY) {
|
||||||
return items.filter((item) => !item.site && item.mode !== "inference");
|
return items.filter(
|
||||||
|
(item) => item.sites.length === 0 && item.mode !== "inference"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const siteId = Number.parseInt(groupKey, 10);
|
const siteId = Number.parseInt(groupKey, 10);
|
||||||
if (!Number.isFinite(siteId)) {
|
if (!Number.isFinite(siteId)) {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return items.filter((item) => item.site?.siteId === siteId);
|
return items.filter((item) =>
|
||||||
|
item.sites.some((site) => site.siteId === siteId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterResourcesByLabel(
|
function filterResourcesByLabel(
|
||||||
@@ -1499,6 +1578,7 @@ async function collectAccessibleSites(
|
|||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
name: sites.name,
|
name: sites.name,
|
||||||
|
niceId: sites.niceId,
|
||||||
type: sites.type,
|
type: sites.type,
|
||||||
online: sites.online,
|
online: sites.online,
|
||||||
itemCount: countDistinct(resources.resourceId)
|
itemCount: countDistinct(resources.resourceId)
|
||||||
@@ -1507,7 +1587,13 @@ async function collectAccessibleSites(
|
|||||||
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||||
.where(and(...publicConditions))
|
.where(and(...publicConditions))
|
||||||
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
.groupBy(
|
||||||
|
sites.siteId,
|
||||||
|
sites.name,
|
||||||
|
sites.niceId,
|
||||||
|
sites.type,
|
||||||
|
sites.online
|
||||||
|
);
|
||||||
|
|
||||||
for (const row of publicRows) {
|
for (const row of publicRows) {
|
||||||
const existing = siteCountMap.get(row.siteId);
|
const existing = siteCountMap.get(row.siteId);
|
||||||
@@ -1517,6 +1603,7 @@ async function collectAccessibleSites(
|
|||||||
siteCountMap.set(row.siteId, {
|
siteCountMap.set(row.siteId, {
|
||||||
siteId: row.siteId,
|
siteId: row.siteId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
niceId: row.niceId,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
online: row.online,
|
online: row.online,
|
||||||
itemCount: Number(row.itemCount)
|
itemCount: Number(row.itemCount)
|
||||||
@@ -1540,6 +1627,7 @@ async function collectAccessibleSites(
|
|||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
name: sites.name,
|
name: sites.name,
|
||||||
|
niceId: sites.niceId,
|
||||||
type: sites.type,
|
type: sites.type,
|
||||||
online: sites.online,
|
online: sites.online,
|
||||||
itemCount: countDistinct(siteResources.siteResourceId)
|
itemCount: countDistinct(siteResources.siteResourceId)
|
||||||
@@ -1551,7 +1639,13 @@ async function collectAccessibleSites(
|
|||||||
)
|
)
|
||||||
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
||||||
.where(and(...siteConditions))
|
.where(and(...siteConditions))
|
||||||
.groupBy(sites.siteId, sites.name, sites.type, sites.online);
|
.groupBy(
|
||||||
|
sites.siteId,
|
||||||
|
sites.name,
|
||||||
|
sites.niceId,
|
||||||
|
sites.type,
|
||||||
|
sites.online
|
||||||
|
);
|
||||||
|
|
||||||
for (const row of siteRows) {
|
for (const row of siteRows) {
|
||||||
const existing = siteCountMap.get(row.siteId);
|
const existing = siteCountMap.get(row.siteId);
|
||||||
@@ -1561,6 +1655,7 @@ async function collectAccessibleSites(
|
|||||||
siteCountMap.set(row.siteId, {
|
siteCountMap.set(row.siteId, {
|
||||||
siteId: row.siteId,
|
siteId: row.siteId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
niceId: row.niceId,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
online: row.online,
|
online: row.online,
|
||||||
itemCount: Number(row.itemCount)
|
itemCount: Number(row.itemCount)
|
||||||
@@ -1675,6 +1770,7 @@ export async function listAccessibleLauncherSitesForUser(
|
|||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
siteId: row.siteId,
|
siteId: row.siteId,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
|
niceId: row.niceId,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
online: row.online
|
online: row.online
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type LauncherLabel = {
|
|||||||
export type LauncherSiteInfo = {
|
export type LauncherSiteInfo = {
|
||||||
siteId: number;
|
siteId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
niceId: string;
|
||||||
type: string;
|
type: string;
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
};
|
};
|
||||||
@@ -51,6 +52,7 @@ export type LauncherResource = {
|
|||||||
mode: string;
|
mode: string;
|
||||||
labels: LauncherLabel[];
|
labels: LauncherLabel[];
|
||||||
site?: LauncherSiteInfo;
|
site?: LauncherSiteInfo;
|
||||||
|
sites: LauncherSiteInfo[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LauncherGroup = {
|
export type LauncherGroup = {
|
||||||
@@ -184,8 +186,7 @@ export function parseIdListParam(value: string | undefined): number[] {
|
|||||||
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
|
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
|
||||||
|
|
||||||
export type LauncherViewSelection =
|
export type LauncherViewSelection =
|
||||||
| { type: "default" }
|
{ type: "default" } | { type: "saved"; viewId: number };
|
||||||
| { type: "saved"; viewId: number };
|
|
||||||
|
|
||||||
export type LauncherScaleCapabilities = {
|
export type LauncherScaleCapabilities = {
|
||||||
allowSiteGrouping: boolean;
|
allowSiteGrouping: boolean;
|
||||||
|
|||||||
@@ -28,7 +28,22 @@ import {
|
|||||||
} from "@app/components/SidePanel";
|
} from "@app/components/SidePanel";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@app/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from "@app/components/ui/table";
|
||||||
import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
|
import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
import {
|
import {
|
||||||
derivePublicAuthState,
|
derivePublicAuthState,
|
||||||
formatPublicResourceType
|
formatPublicResourceType
|
||||||
@@ -36,7 +51,10 @@ import {
|
|||||||
import { getLauncherResourceAdminHref } from "@app/lib/launcherResourceAdminHref";
|
import { getLauncherResourceAdminHref } from "@app/lib/launcherResourceAdminHref";
|
||||||
import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess";
|
import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess";
|
||||||
import { launcherQueries } from "@app/lib/queries";
|
import { launcherQueries } from "@app/lib/queries";
|
||||||
import type { LauncherResource } from "@server/routers/launcher/types";
|
import type {
|
||||||
|
LauncherResource,
|
||||||
|
LauncherSiteInfo
|
||||||
|
} from "@server/routers/launcher/types";
|
||||||
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
|
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
|
||||||
import type { GetResourceResponse } from "@server/routers/resource/getResource";
|
import type { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||||
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
|
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
|
||||||
@@ -44,15 +62,18 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
ChevronsUpDown,
|
||||||
Clock,
|
Clock,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
MoreHorizontal,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
ShieldOff,
|
ShieldOff,
|
||||||
XCircle
|
XCircle
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
type LauncherResourcePanelProps = {
|
type LauncherResourcePanelProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -60,6 +81,7 @@ type LauncherResourcePanelProps = {
|
|||||||
resource: LauncherResource | null;
|
resource: LauncherResource | null;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LauncherResourceDetailResult =
|
type LauncherResourceDetailResult =
|
||||||
@@ -153,6 +175,171 @@ function HealthStatusDisplay({
|
|||||||
const PUBLIC_AUTH_METHODS_MODES = ["http", "ssh", "rdp", "vnc"];
|
const PUBLIC_AUTH_METHODS_MODES = ["http", "ssh", "rdp", "vnc"];
|
||||||
const PUBLIC_AUTH_BADGE_MODES = [...PUBLIC_AUTH_METHODS_MODES, "inference"];
|
const PUBLIC_AUTH_BADGE_MODES = [...PUBLIC_AUTH_METHODS_MODES, "inference"];
|
||||||
|
|
||||||
|
type SitesSortKey = "name" | "status";
|
||||||
|
type SitesSortOrder = "asc" | "desc";
|
||||||
|
|
||||||
|
function siteStatusSortValue(site: LauncherSiteInfo): number {
|
||||||
|
if (site.type !== "newt" && site.type !== "wireguard") {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (typeof site.online !== "boolean") {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return site.online ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SiteStatusCell({ site }: { site: LauncherSiteInfo }) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
if (site.type !== "newt" && site.type !== "wireguard") {
|
||||||
|
return <span>-</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof site.online !== "boolean") {
|
||||||
|
return <span>-</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"size-2 shrink-0 rounded-full",
|
||||||
|
site.online ? "bg-green-500" : "bg-neutral-500"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>{site.online ? t("online") : t("offline")}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LauncherResourceSitesSection({
|
||||||
|
orgId,
|
||||||
|
sites,
|
||||||
|
isAdmin,
|
||||||
|
onFilterBySite
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
sites: LauncherSiteInfo[];
|
||||||
|
isAdmin: boolean;
|
||||||
|
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||||
|
}) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [sortKey, setSortKey] = useState<SitesSortKey>("name");
|
||||||
|
const [sortOrder, setSortOrder] = useState<SitesSortOrder>("asc");
|
||||||
|
|
||||||
|
if (sites.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSort(key: SitesSortKey) {
|
||||||
|
if (sortKey === key) {
|
||||||
|
setSortOrder((order) => (order === "asc" ? "desc" : "asc"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSortKey(key);
|
||||||
|
setSortOrder("asc");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedSites = [...sites].sort((a, b) => {
|
||||||
|
const cmp =
|
||||||
|
sortKey === "name"
|
||||||
|
? a.name.localeCompare(b.name, undefined, {
|
||||||
|
sensitivity: "base"
|
||||||
|
})
|
||||||
|
: siteStatusSortValue(a) - siteStatusSortValue(b);
|
||||||
|
return sortOrder === "desc" ? -cmp : cmp;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>{t("sites")}</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("resourceLauncherSitesDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-3"
|
||||||
|
onClick={() => toggleSort("name")}
|
||||||
|
>
|
||||||
|
{t("name")}
|
||||||
|
<ChevronsUpDown className="ml-2 size-4" />
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-3"
|
||||||
|
onClick={() => toggleSort("status")}
|
||||||
|
>
|
||||||
|
{t("status")}
|
||||||
|
<ChevronsUpDown className="ml-2 size-4" />
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="w-12">
|
||||||
|
<span className="sr-only">{t("actions")}</span>
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{sortedSites.map((site) => (
|
||||||
|
<TableRow key={site.siteId}>
|
||||||
|
<TableCell>{site.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<SiteStatusCell site={site} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">
|
||||||
|
{t("openMenu")}
|
||||||
|
</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{isAdmin ? (
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={`/${orgId}/settings/sites/${site.niceId}`}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"resourceLauncherViewSiteAsAdmin"
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
onFilterBySite(site)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"resourceLauncherFilterBySite"
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AuthMethodStatusDisplay({ enabled }: { enabled: boolean }) {
|
function AuthMethodStatusDisplay({ enabled }: { enabled: boolean }) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -235,12 +422,16 @@ function PublicResourceDetails({
|
|||||||
orgId,
|
orgId,
|
||||||
launcherResource,
|
launcherResource,
|
||||||
resource,
|
resource,
|
||||||
authInfo
|
authInfo,
|
||||||
|
isAdmin,
|
||||||
|
onFilterBySite
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
launcherResource: LauncherResource;
|
launcherResource: LauncherResource;
|
||||||
resource: GetResourceResponse;
|
resource: GetResourceResponse;
|
||||||
authInfo: GetResourceAuthInfoResponse;
|
authInfo: GetResourceAuthInfoResponse;
|
||||||
|
isAdmin: boolean;
|
||||||
|
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const mode = resource.mode || "";
|
const mode = resource.mode || "";
|
||||||
@@ -328,6 +519,12 @@ function PublicResourceDetails({
|
|||||||
</InfoSections>
|
</InfoSections>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
<LauncherResourceSitesSection
|
||||||
|
orgId={orgId}
|
||||||
|
sites={launcherResource.sites ?? []}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
onFilterBySite={onFilterBySite}
|
||||||
|
/>
|
||||||
{showAuthMethods ? (
|
{showAuthMethods ? (
|
||||||
<PublicResourceAuthMethods authInfo={authInfo} />
|
<PublicResourceAuthMethods authInfo={authInfo} />
|
||||||
) : null}
|
) : null}
|
||||||
@@ -363,11 +560,15 @@ function PublicResourceDetails({
|
|||||||
function PrivateResourceDetails({
|
function PrivateResourceDetails({
|
||||||
orgId,
|
orgId,
|
||||||
launcherResource,
|
launcherResource,
|
||||||
resource
|
resource,
|
||||||
|
isAdmin,
|
||||||
|
onFilterBySite
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
launcherResource: LauncherResource;
|
launcherResource: LauncherResource;
|
||||||
resource: GetSiteResourceResponse;
|
resource: GetSiteResourceResponse;
|
||||||
|
isAdmin: boolean;
|
||||||
|
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const isInference = resource.mode === "inference";
|
const isInference = resource.mode === "inference";
|
||||||
@@ -417,6 +618,12 @@ function PrivateResourceDetails({
|
|||||||
/>
|
/>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
<LauncherResourceSitesSection
|
||||||
|
orgId={orgId}
|
||||||
|
sites={launcherResource.sites ?? []}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
onFilterBySite={onFilterBySite}
|
||||||
|
/>
|
||||||
{isInference ? (
|
{isInference ? (
|
||||||
<>
|
<>
|
||||||
<LauncherInferenceModelsSection
|
<LauncherInferenceModelsSection
|
||||||
@@ -440,11 +647,15 @@ function PrivateResourceDetails({
|
|||||||
function LauncherResourcePanelBody({
|
function LauncherResourcePanelBody({
|
||||||
orgId,
|
orgId,
|
||||||
resource,
|
resource,
|
||||||
open
|
open,
|
||||||
|
isAdmin,
|
||||||
|
onFilterBySite
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
resource: LauncherResource;
|
resource: LauncherResource;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { data, isPending, isError } = useQuery({
|
const { data, isPending, isError } = useQuery({
|
||||||
@@ -477,6 +688,8 @@ function LauncherResourcePanelBody({
|
|||||||
launcherResource={resource}
|
launcherResource={resource}
|
||||||
resource={detail.data}
|
resource={detail.data}
|
||||||
authInfo={detail.authInfo}
|
authInfo={detail.authInfo}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
onFilterBySite={onFilterBySite}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -486,6 +699,8 @@ function LauncherResourcePanelBody({
|
|||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
launcherResource={resource}
|
launcherResource={resource}
|
||||||
resource={detail.data}
|
resource={detail.data}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
onFilterBySite={onFilterBySite}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -495,7 +710,8 @@ export function LauncherResourcePanel({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
resource,
|
resource,
|
||||||
orgId,
|
orgId,
|
||||||
isAdmin
|
isAdmin,
|
||||||
|
onFilterBySite
|
||||||
}: LauncherResourcePanelProps) {
|
}: LauncherResourcePanelProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -511,6 +727,8 @@ export function LauncherResourcePanel({
|
|||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
open={open}
|
open={open}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
onFilterBySite={onFilterBySite}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</SidePanelBody>
|
</SidePanelBody>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
type LauncherGroup,
|
type LauncherGroup,
|
||||||
type LauncherResource,
|
type LauncherResource,
|
||||||
type LauncherScaleInfo,
|
type LauncherScaleInfo,
|
||||||
|
type LauncherSiteInfo,
|
||||||
type LauncherViewConfig,
|
type LauncherViewConfig,
|
||||||
type LauncherViewRecord,
|
type LauncherViewRecord,
|
||||||
type ListLauncherResourcesResponse
|
type ListLauncherResourcesResponse
|
||||||
@@ -584,6 +585,14 @@ export default function ResourceLauncher({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleFilterBySite = useCallback(
|
||||||
|
(site: LauncherSiteInfo) => {
|
||||||
|
applyConfigPatch({ siteIds: [site.siteId] });
|
||||||
|
handlePanelOpenChange(false);
|
||||||
|
},
|
||||||
|
[applyConfigPatch, handlePanelOpenChange]
|
||||||
|
);
|
||||||
|
|
||||||
const hasHandledAutoOpen = useRef(false);
|
const hasHandledAutoOpen = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -795,6 +804,7 @@ export default function ResourceLauncher({
|
|||||||
resource={selectedResource}
|
resource={selectedResource}
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
|
onFilterBySite={handleFilterBySite}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{activeSavedView ? (
|
{activeSavedView ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user