Skip to content

Commit 38a0329

Browse files
Show targets and status icons in the dashboard
1 parent 1f1c20d commit 38a0329

File tree

9 files changed

+889
-113
lines changed

9 files changed

+889
-113
lines changed

messages/en-US.json

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1496,5 +1496,75 @@
14961496
"convertButton": "Convert This Node to Managed Self-Hosted"
14971497
},
14981498
"internationaldomaindetected": "International Domain Detected",
1499-
"willbestoredas": "Will be stored as:"
1499+
"willbestoredas": "Will be stored as:",
1500+
"target": "Target",
1501+
"checking": "Checking",
1502+
"resourceDisabled": "This resource is currently disabled",
1503+
"checkingConnection": "Checking connection status...",
1504+
"connectionSuccessful": "Connection successful",
1505+
"connectionFailed": "Connection failed - target may be unreachable",
1506+
"statusUnknown": "Status unknown",
1507+
"targetNotConfigured": "Target not configured",
1508+
"tcpCheck": "TCP Check",
1509+
"connectionStatus": "Connection Status",
1510+
"responseTime": "Response Time",
1511+
"lastChecked": "Last Checked",
1512+
"refreshStatus": "Refresh Status",
1513+
"showColumns": "Show Columns",
1514+
"hideColumns": "Hide Columns",
1515+
"columnVisibility": "Column Visibility",
1516+
"toggleColumn": "Toggle {columnName} column",
1517+
"allColumns": "All Columns",
1518+
"defaultColumns": "Default Columns",
1519+
"customizeView": "Customize View",
1520+
"viewOptions": "View Options",
1521+
"statusIndicators": "Status Indicators",
1522+
"quickHealthCheck": "Quick Health Check",
1523+
"networkConnectivity": "Network Connectivity",
1524+
"targetReachable": "Target is reachable",
1525+
"targetUnreachable": "Target is unreachable",
1526+
"connectivityError": "Connectivity error: {error}",
1527+
"checkingAllResources": "Checking all resources...",
1528+
"healthCheckComplete": "Health check complete",
1529+
"resourcesOnline": "{count} resources online",
1530+
"resourcesOffline": "{count} resources offline",
1531+
"batchStatusCheck": "Batch Status Check",
1532+
"runHealthCheck": "Run Health Check",
1533+
"autoRefresh": "Auto Refresh",
1534+
"manualRefresh": "Manual Refresh",
1535+
"refreshInterval": "Refresh Interval",
1536+
"statusRefreshed": "Status refreshed successfully",
1537+
"statusRefreshFailed": "Failed to refresh status",
1538+
"tcpConnectionTest": "TCP Connection Test",
1539+
"portConnectivity": "Port Connectivity",
1540+
"networkLatency": "Network Latency",
1541+
"ms": "ms",
1542+
"seconds": "seconds",
1543+
"timeout": "Timeout",
1544+
"connectionTimeout": "Connection timeout after {timeout}ms",
1545+
"dnsBased": "DNS-based",
1546+
"ipBased": "IP-based",
1547+
"localhost": "Localhost",
1548+
"remoteHost": "Remote Host",
1549+
"showStatusColumn": "Show status indicators for quick health visibility",
1550+
"showTargetColumn": "Show target destinations to see where resources point",
1551+
"customizeColumns": "Customize which columns are visible in the table",
1552+
"statusTooltip": "Click to refresh status for this resource",
1553+
"targetTooltip": "The destination that this resource proxies to",
1554+
"bulkActions": "Bulk Actions",
1555+
"selectAll": "Select All",
1556+
"selectNone": "Select None",
1557+
"selectedResources": "Selected Resources",
1558+
"enableSelected": "Enable Selected",
1559+
"disableSelected": "Disable Selected",
1560+
"checkSelectedStatus": "Check Status of Selected",
1561+
"exportSelected": "Export Selected",
1562+
"healthDashboard": "Health Dashboard",
1563+
"systemHealth": "System Health",
1564+
"resourcesHealthSummary": "Resources Health Summary",
1565+
"overallHealth": "Overall Health",
1566+
"healthy": "Healthy",
1567+
"degraded": "Degraded",
1568+
"critical": "Critical",
1569+
"monitoringNote": "Status indicators provide a quick TCP connectivity check refreshed when the page loads. This is not full monitoring but helps identify unreachable targets at a glance."
15001570
}

server/auth/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export enum ActionsEnum {
2323
deleteResource = "deleteResource",
2424
getResource = "getResource",
2525
listResources = "listResources",
26+
tcpCheck = "tcpCheck",
27+
batchTcpCheck = "batchTcpCheck",
2628
updateResource = "updateResource",
2729
createTarget = "createTarget",
2830
deleteTarget = "deleteTarget",

server/routers/external.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,20 @@ authenticated.get(
290290
resource.listResources
291291
);
292292

293+
authenticated.post(
294+
"/org/:orgId/resources/tcp-check",
295+
verifyOrgAccess,
296+
verifyUserHasAction(ActionsEnum.tcpCheck),
297+
resource.tcpCheck
298+
);
299+
300+
authenticated.post(
301+
"/org/:orgId/resources/tcp-check-batch",
302+
verifyOrgAccess,
303+
verifyUserHasAction(ActionsEnum.batchTcpCheck),
304+
resource.batchTcpCheck
305+
);
306+
293307
authenticated.get(
294308
"/org/:orgId/user-resources",
295309
verifyOrgAccess,

server/routers/resource/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ export * from "./deleteResourceRule";
2222
export * from "./listResourceRules";
2323
export * from "./updateResourceRule";
2424
export * from "./getUserResources";
25+
export * from "./tcpCheck";

server/routers/resource/listResources.ts

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
userResources,
77
roleResources,
88
resourcePassword,
9-
resourcePincode
9+
resourcePincode,
10+
targets,
1011
} from "@server/db";
1112
import response from "@server/lib/response";
1213
import HttpCode from "@server/types/HttpCode";
@@ -39,6 +40,51 @@ const listResourcesSchema = z.object({
3940
.pipe(z.number().int().nonnegative())
4041
});
4142

43+
// (resource fields + a single joined target)
44+
type JoinedRow = {
45+
resourceId: number;
46+
name: string;
47+
ssl: boolean;
48+
fullDomain: string | null;
49+
passwordId: number | null;
50+
sso: boolean;
51+
pincodeId: number | null;
52+
whitelist: boolean;
53+
http: boolean;
54+
protocol: string;
55+
proxyPort: number | null;
56+
enabled: boolean;
57+
domainId: string | null;
58+
59+
targetId: number | null;
60+
targetIp: string | null;
61+
targetPort: number | null;
62+
targetEnabled: boolean | null;
63+
};
64+
65+
// grouped by resource with targets[])
66+
export type ResourceWithTargets = {
67+
resourceId: number;
68+
name: string;
69+
ssl: boolean;
70+
fullDomain: string | null;
71+
passwordId: number | null;
72+
sso: boolean;
73+
pincodeId: number | null;
74+
whitelist: boolean;
75+
http: boolean;
76+
protocol: string;
77+
proxyPort: number | null;
78+
enabled: boolean;
79+
domainId: string | null;
80+
targets: Array<{
81+
targetId: number;
82+
ip: string;
83+
port: number;
84+
enabled: boolean;
85+
}>;
86+
};
87+
4288
function queryResources(accessibleResourceIds: number[], orgId: string) {
4389
return db
4490
.select({
@@ -54,7 +100,12 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
54100
protocol: resources.protocol,
55101
proxyPort: resources.proxyPort,
56102
enabled: resources.enabled,
57-
domainId: resources.domainId
103+
domainId: resources.domainId,
104+
105+
targetId: targets.targetId,
106+
targetIp: targets.ip,
107+
targetPort: targets.port,
108+
targetEnabled: targets.enabled,
58109
})
59110
.from(resources)
60111
.leftJoin(
@@ -65,6 +116,7 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
65116
resourcePincode,
66117
eq(resourcePincode.resourceId, resources.resourceId)
67118
)
119+
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
68120
.where(
69121
and(
70122
inArray(resources.resourceId, accessibleResourceIds),
@@ -74,7 +126,7 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
74126
}
75127

76128
export type ListResourcesResponse = {
77-
resources: NonNullable<Awaited<ReturnType<typeof queryResources>>>;
129+
resources: ResourceWithTargets[];
78130
pagination: { total: number; limit: number; offset: number };
79131
};
80132

@@ -139,7 +191,7 @@ export async function listResources(
139191
);
140192
}
141193

142-
let accessibleResources;
194+
let accessibleResources: Array<{ resourceId: number }>;
143195
if (req.user) {
144196
accessibleResources = await db
145197
.select({
@@ -176,9 +228,48 @@ export async function listResources(
176228

177229
const baseQuery = queryResources(accessibleResourceIds, orgId);
178230

179-
const resourcesList = await baseQuery!.limit(limit).offset(offset);
231+
const rows: JoinedRow[] = await baseQuery.limit(limit).offset(offset);
232+
233+
// avoids TS issues with reduce/never[]
234+
const map = new Map<number, ResourceWithTargets>();
235+
236+
for (const row of rows) {
237+
let entry = map.get(row.resourceId);
238+
if (!entry) {
239+
entry = {
240+
resourceId: row.resourceId,
241+
name: row.name,
242+
ssl: row.ssl,
243+
fullDomain: row.fullDomain,
244+
passwordId: row.passwordId,
245+
sso: row.sso,
246+
pincodeId: row.pincodeId,
247+
whitelist: row.whitelist,
248+
http: row.http,
249+
protocol: row.protocol,
250+
proxyPort: row.proxyPort,
251+
enabled: row.enabled,
252+
domainId: row.domainId,
253+
targets: [],
254+
};
255+
map.set(row.resourceId, entry);
256+
}
257+
258+
// Push target if present (left join can be null)
259+
if (row.targetId != null && row.targetIp && row.targetPort != null && row.targetEnabled != null) {
260+
entry.targets.push({
261+
targetId: row.targetId,
262+
ip: row.targetIp,
263+
port: row.targetPort,
264+
enabled: row.targetEnabled,
265+
});
266+
}
267+
}
268+
269+
const resourcesList: ResourceWithTargets[] = Array.from(map.values());
270+
180271
const totalCountResult = await countQuery;
181-
const totalCount = totalCountResult[0].count;
272+
const totalCount = totalCountResult[0]?.count ?? 0;
182273

183274
return response<ListResourcesResponse>(res, {
184275
data: {

0 commit comments

Comments
 (0)