Skip to content

Commit c6bafa3

Browse files
authored
[Feature] Make Ray and Logs links proxy to their Ray dashboards (#4112)
* refactor(dashboard): separate API domain and paths, add proxy / event api path support Signed-off-by: Cheyu Wu <[email protected]> * feat: rayjob / raycluster table can redirect to raydashboard Signed-off-by: Cheyu Wu <[email protected]> --------- Signed-off-by: Cheyu Wu <[email protected]>
1 parent 1d04c34 commit c6bafa3

File tree

11 files changed

+137
-48
lines changed

11 files changed

+137
-48
lines changed
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { NextResponse } from "next/server";
2-
import { defaultConfig } from "@/utils/constants";
2+
import { getServerConfig } from "@/utils/config-server";
33

44
export async function GET() {
5-
const config = {
6-
apiUrl:
7-
process.env.NEXT_PUBLIC_API_URL ||
8-
process.env.API_URL ||
9-
defaultConfig.url,
10-
};
11-
5+
const config = getServerConfig();
126
return NextResponse.json(config);
137
}

dashboard/src/components/JobsTable/JobsTable.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export const JobsTable = () => {
9494
</td>
9595
<td>{dayjs(row.createdAt).format("M/D/YY HH:mm:ss")}</td>
9696
<td className="flex">
97-
{row.jobStatus.jobStatus === "RUNNING" && (
97+
{
9898
<IconButton
9999
variant="plain"
100100
size="sm"
@@ -103,13 +103,6 @@ export const JobsTable = () => {
103103
href={row.links.rayHeadDashboardLink}
104104
target="_blank"
105105
component="a"
106-
onClick={() => {
107-
snackBar.showSnackBar(
108-
"Ray Dashboard not available",
109-
"We are working on exposing the dashboard securely without slowing down jobs. Apologies for the inconvenience.",
110-
"warning",
111-
);
112-
}}
113106
>
114107
<Image
115108
priority
@@ -119,7 +112,7 @@ export const JobsTable = () => {
119112
width={26}
120113
/>
121114
</IconButton>
122-
)}
115+
}
123116
{row.links.rayGrafanaDashboardLink && (
124117
<IconButton
125118
variant="plain"

dashboard/src/hooks/api/useCreateJob.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ async function _createJob(
5252
// ]
5353
// }
5454
// }'
55-
const url = `${config.url}/namespaces/${namespace}/rayjobs`;
55+
const url = `${config.rayApiUrl}/namespaces/${namespace}/rayjobs`;
5656
const data = {
5757
name: jobName,
5858
namespace: "default",

dashboard/src/hooks/api/useDeleteClusters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useNamespace } from "@/components/NamespaceProvider";
55
import { config } from "@/utils/constants";
66

77
async function _deleteCluster(namespace: string, clusterName: string) {
8-
const baseUrl = `${config.url}/namespaces/${namespace}/rayclusters/`;
8+
const baseUrl = `${config.rayApiUrl}/namespaces/${namespace}/rayclusters/`;
99
const response = await fetch(`${baseUrl}${clusterName}`, {
1010
method: "DELETE",
1111
});

dashboard/src/hooks/api/useDeleteJobs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useNamespace } from "@/components/NamespaceProvider";
55
import { config } from "@/utils/constants";
66

77
async function _deleteJob(namespace: string, jobName: string) {
8-
const baseUrl = `${config.url}/namespaces/${namespace}/rayjobs/`;
8+
const baseUrl = `${config.rayApiUrl}/namespaces/${namespace}/rayjobs/`;
99
const response = await fetch(`${baseUrl}${jobName}`, {
1010
method: "DELETE",
1111
});

dashboard/src/hooks/api/useListClusters.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { ALL_NAMESPACES } from "@/utils/constants";
21
import { useNamespace } from "@/components/NamespaceProvider";
32
import fetcher from "@/utils/fetch";
43
import useSWR from "swr";
@@ -7,6 +6,8 @@ import { RayClusterListResponse } from "@/types/v2/api/raycluster";
76
import { ClusterRow } from "@/types/table";
87
import { ClusterStatus } from "@/types/v2/raycluster";
98
import { V1Condition } from "@kubernetes/client-node";
9+
import { ALL_NAMESPACES } from "@/utils/config-defaults";
10+
import { config } from "@/utils/constants";
1011

1112
export const useListClusters = (
1213
refreshInterval: number = 5000,
@@ -74,18 +75,28 @@ const transformRayClusterResponse = (
7475
): ClusterRow[] => {
7576
return response.items.map((item) => {
7677
const clusterState = parseClusterStatus(item.status.conditions ?? []);
78+
const namespace = item.metadata.namespace!;
7779

7880
const generateLinks = () => {
79-
const serviceIP = item.status.head?.serviceIP ?? "";
80-
const dashboardPort = item.status.endpoints?.dashboard ?? "";
81+
const serviceName = item.status.head?.serviceName ?? "";
82+
const dashboardPort =
83+
item.spec.headGroupSpec?.template?.spec?.containers?.[0].ports?.find(
84+
(port) => port.name === "dashboard",
85+
)?.containerPort;
86+
87+
if (!serviceName || !dashboardPort) {
88+
return {
89+
rayHeadDashboardLink: "",
90+
};
91+
}
8192
return {
82-
rayHeadDashboardLink: `http://${serviceIP}:${dashboardPort}`,
93+
rayHeadDashboardLink: `${config.coreApiUrl}/namespaces/${namespace}/services/${serviceName}:${dashboardPort}/proxy/#/cluster`,
8394
};
8495
};
8596

8697
return {
8798
name: item.metadata.name!,
88-
namespace: item.metadata.namespace!,
99+
namespace,
89100
rayVersion: item.spec.rayVersion,
90101
clusterState,
91102
createdAt: item.metadata.creationTimestamp,

dashboard/src/hooks/api/useListJobs.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { ALL_NAMESPACES } from "@/utils/constants";
21
import { useNamespace } from "@/components/NamespaceProvider";
32
import fetcher from "@/utils/fetch";
43
import useSWR from "swr";
54
import { RayJobListResponse, RayJobItem } from "@/types/v2/api/rayjob";
65
import { JobRow } from "@/types/table";
6+
import { ALL_NAMESPACES } from "@/utils/config-defaults";
7+
import { config } from "@/utils/constants";
78

89
export const useListJobs = (
910
refreshInterval: number = 5000,
@@ -42,9 +43,23 @@ export const useListJobs = (
4243
};
4344

4445
const convertRayJobItemToJobRow = (item: RayJobItem): JobRow => {
46+
const namespace = item.metadata.namespace!;
4547
const generateLinks = () => {
48+
const serviceName = item.status?.rayClusterStatus?.head?.serviceName;
49+
const dashboardPort =
50+
item.spec.rayClusterSpec.headGroupSpec?.template?.spec?.containers?.[0].ports?.find(
51+
(port) => port.name === "dashboard",
52+
)?.containerPort;
53+
if (!dashboardPort || !config.coreApiUrl) {
54+
return {
55+
rayHeadDashboardLink: "",
56+
};
57+
}
58+
const rayHeadDashboardLink = `${config.coreApiUrl}/namespaces/${namespace}/services/${serviceName}:${dashboardPort}/proxy/#/jobs`;
59+
const rayJobId = item.status?.jobId;
4660
return {
47-
rayHeadDashboardLink: `http://${item.status.dashboardURL}`,
61+
rayHeadDashboardLink,
62+
logsLink: `${rayHeadDashboardLink}/${rayJobId}`,
4863
};
4964
};
5065
return {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export interface RuntimeConfig {
2+
domain: string;
3+
rayApiPath: string;
4+
coreApiPath?: string;
5+
}
6+
7+
export const apiVersion = "v2";
8+
9+
export const defaultDomain = "http://localhost:31888";
10+
export const v2RayApiPath = "/apis/ray.io/v1";
11+
export const v1RayApiPath = "/apis/v1";
12+
export const v2CoreApiPath = "/api/v1";
13+
14+
export const defaultConfig: RuntimeConfig = {
15+
domain: defaultDomain,
16+
rayApiPath: apiVersion === "v2" ? v2RayApiPath : v1RayApiPath,
17+
coreApiPath: apiVersion === "v2" ? v2CoreApiPath : undefined,
18+
};
19+
20+
export const ALL_NAMESPACES = "all";
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
defaultConfig,
3+
apiVersion,
4+
type RuntimeConfig,
5+
} from "./config-defaults";
6+
7+
export function getServerConfig(): RuntimeConfig {
8+
// Support legacy API_URL env var for backward compatibility
9+
const legacyApiUrl = process.env.NEXT_PUBLIC_API_URL || process.env.API_URL;
10+
11+
if (legacyApiUrl) {
12+
// Parse legacy URL format
13+
try {
14+
const url = new URL(legacyApiUrl);
15+
return {
16+
domain: `${url.protocol}//${url.host}`,
17+
rayApiPath: url.pathname,
18+
coreApiPath: apiVersion === "v2" ? "/api/v1" : undefined,
19+
};
20+
} catch {
21+
// If not a valid URL, return as domain
22+
return {
23+
domain: legacyApiUrl,
24+
rayApiPath: defaultConfig.rayApiPath,
25+
coreApiPath: defaultConfig.coreApiPath,
26+
};
27+
}
28+
}
29+
30+
// New format with separated domain and path
31+
return {
32+
domain:
33+
process.env.NEXT_PUBLIC_API_DOMAIN ||
34+
process.env.API_DOMAIN ||
35+
defaultConfig.domain,
36+
rayApiPath:
37+
process.env.NEXT_PUBLIC_RAY_API_PATH ||
38+
process.env.RAY_API_PATH ||
39+
defaultConfig.rayApiPath,
40+
coreApiPath:
41+
process.env.NEXT_PUBLIC_CORE_API_PATH ||
42+
process.env.CORE_API_PATH ||
43+
defaultConfig.coreApiPath,
44+
};
45+
}

dashboard/src/utils/constants.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
1-
export const ALL_NAMESPACES = "all";
1+
import { RuntimeConfig, defaultConfig, apiVersion } from "./config-defaults";
22

3-
interface RuntimeConfig {
4-
url: string;
5-
}
6-
7-
export const apiVersion = "v2";
8-
9-
export const defaultConfig: RuntimeConfig = {
10-
url:
11-
apiVersion === "v2"
12-
? "http://localhost:31888/apis/ray.io/v1"
13-
: "http://localhost:31888/apis/v1",
14-
};
3+
export { defaultConfig, apiVersion };
4+
export type { RuntimeConfig };
155

166
let runtimeConfig: RuntimeConfig | null = null;
177

@@ -23,29 +13,50 @@ export async function fetchRuntimeConfig(): Promise<RuntimeConfig> {
2313
try {
2414
const response = await fetch("/api/config");
2515
if (response.ok) {
26-
const data = await response.json();
16+
const data: RuntimeConfig = await response.json();
2717
runtimeConfig = {
28-
url: data.apiUrl || defaultConfig.url,
18+
domain: data.domain || defaultConfig.domain,
19+
rayApiPath: data.rayApiPath || defaultConfig.rayApiPath,
20+
coreApiPath:
21+
data.coreApiPath !== undefined
22+
? data.coreApiPath
23+
: defaultConfig.coreApiPath,
2924
};
3025
return runtimeConfig;
3126
}
3227
} catch (error) {
3328
console.warn("Failed to fetch runtime config, using default:", error);
3429
}
3530

36-
// Fallback to default config
3731
runtimeConfig = defaultConfig;
3832
return runtimeConfig;
3933
}
4034

4135
export const config = {
42-
async getUrl(): Promise<string> {
36+
async getRayApiUrl(): Promise<string> {
4337
const cfg = await fetchRuntimeConfig();
44-
return cfg.url;
38+
return `${cfg.domain}${cfg.rayApiPath}`;
4539
},
4640

47-
get url(): string {
48-
return runtimeConfig?.url || defaultConfig.url;
41+
async getCoreApiUrl(): Promise<string | undefined> {
42+
const cfg = await fetchRuntimeConfig();
43+
return cfg.coreApiPath ? `${cfg.domain}${cfg.coreApiPath}` : undefined;
44+
},
45+
46+
get rayApiUrl(): string {
47+
if (runtimeConfig) {
48+
return `${runtimeConfig.domain}${runtimeConfig.rayApiPath}`;
49+
}
50+
return `${defaultConfig.domain}${defaultConfig.rayApiPath}`;
51+
},
52+
53+
get coreApiUrl(): string | undefined {
54+
if (runtimeConfig?.coreApiPath) {
55+
return `${runtimeConfig.domain}${runtimeConfig.coreApiPath}`;
56+
}
57+
return defaultConfig.coreApiPath
58+
? `${defaultConfig.domain}${defaultConfig.coreApiPath}`
59+
: undefined;
4960
},
5061
};
5162

0 commit comments

Comments
 (0)