Skip to content

Commit 1dfe903

Browse files
authored
Merge branch 'master' into master
2 parents ece64de + 5221b2b commit 1dfe903

22 files changed

Lines changed: 1473 additions & 144 deletions

python/ray/_private/internal_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ def memory_summary(
5353
) + store_stats_summary(reply)
5454

5555

56-
def get_memory_info_reply(state, node_manager_address=None, node_manager_port=None):
56+
def get_memory_info_reply(
57+
state, node_manager_address=None, node_manager_port=None, timeout_seconds=60.0
58+
):
5759
"""Returns global memory info."""
5860

5961
from ray._private.grpc_utils import init_grpc_channel
@@ -86,7 +88,7 @@ def get_memory_info_reply(state, node_manager_address=None, node_manager_port=No
8688
stub = node_manager_pb2_grpc.NodeManagerServiceStub(channel)
8789
reply = stub.FormatGlobalMemoryInfo(
8890
node_manager_pb2.FormatGlobalMemoryInfoRequest(include_memory_info=False),
89-
timeout=60.0,
91+
timeout=timeout_seconds,
9092
)
9193
return reply
9294

python/ray/dashboard/client/src/App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ const App = () => {
218218
useState(false);
219219
const [authenticationError, setAuthenticationError] =
220220
useState<string | undefined>();
221+
221222
useEffect(() => {
222223
getNodeList().then((res) => {
223224
if (res?.data?.data?.summary) {
@@ -381,7 +382,12 @@ const App = () => {
381382
<ThemeProvider theme={currentTheme}>
382383
<Suspense fallback={Loading}>
383384
<GlobalContext.Provider
384-
value={{ ...context, currentTimeZone, themeMode, toggleTheme }}
385+
value={{
386+
...context,
387+
currentTimeZone,
388+
themeMode,
389+
toggleTheme,
390+
}}
385391
>
386392
<CssBaseline />
387393
<TokenAuthenticationDialog
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const API_REFRESH_INTERVAL_MS = 4000;
22
// Per job page refresh interval.
33
export const PER_JOB_PAGE_REFRESH_INTERVAL_MS = 10000;
4+
export const DASHBOARD_DATA_LOADED_EVENT = "ray-dashboard-data-loaded";

python/ray/dashboard/client/src/pages/layout/MainNavLayout.component.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React from "react";
44
import { Link, MemoryRouter, Outlet, Route, Routes } from "react-router-dom";
55
import { STYLE_WRAPPER } from "../../util/test-utils";
66
import { MainNavPageInfo } from "./mainNavContext";
7-
import { MainNavLayout } from "./MainNavLayout";
7+
import { formatFreshnessLabel, MainNavLayout } from "./MainNavLayout";
88

99
const TestPageA = () => {
1010
return (
@@ -109,6 +109,17 @@ const TestApp = ({ location = "/" }: { location?: string }) => {
109109
};
110110

111111
describe("MainNavLayout", () => {
112+
it("formats dashboard data freshness", () => {
113+
expect(formatFreshnessLabel(undefined, 1_000)).toBe("No data loaded");
114+
expect(formatFreshnessLabel(1_000, 5_999)).toBe("Updated just now");
115+
expect(formatFreshnessLabel(1_000, 30_000)).toBe("Updated 30s ago");
116+
expect(formatFreshnessLabel(1_000, 60_999)).toBe("Updated 55s ago");
117+
expect(formatFreshnessLabel(1_000, 61_000)).toBe("Updated 1m ago");
118+
expect(formatFreshnessLabel(1_000, 121_000)).toBe("Updated 2m ago");
119+
expect(formatFreshnessLabel(1_000, 7_201_000)).toBe("Updated 2h ago");
120+
expect(formatFreshnessLabel(1_000, 172_801_000)).toBe("Updated 2d ago");
121+
});
122+
112123
it("navigates and renders breadcrumbs correctly", async () => {
113124
const user = userEvent.setup();
114125

python/ray/dashboard/client/src/pages/layout/MainNavLayout.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import {
44
RiBookMarkLine,
55
RiFeedbackLine,
66
RiMoonLine,
7+
RiRefreshLine,
78
RiSunLine,
89
} from "react-icons/ri/";
910
import { Outlet, Link as RouterLink } from "react-router-dom";
11+
import { useSWRConfig } from "swr";
1012
import { GlobalContext } from "../../App";
13+
import { DASHBOARD_DATA_LOADED_EVENT } from "../../common/constants";
1114
import { SearchTimezone } from "../../components/SearchComponent";
1215
import Logo from "../../logo.svg";
1316
import { MainNavContext, useMainNavState } from "./mainNavContext";
@@ -173,6 +176,7 @@ const MainNavBar = () => {
173176
</Typography>
174177
))}
175178
<Box sx={{ flexGrow: 1 }}></Box>
179+
<DashboardDataFreshness currentTimeZone={currentTimeZone} />
176180
<Box sx={{ marginRight: 2 }}>
177181
{!urlTheme && (
178182
<Tooltip title={themeMode === "light" ? "Dark mode" : "Light mode"}>
@@ -223,6 +227,142 @@ const MainNavBar = () => {
223227
);
224228
};
225229

230+
const DashboardDataFreshness = ({
231+
currentTimeZone,
232+
}: {
233+
currentTimeZone: string | undefined;
234+
}) => {
235+
const { mutate } = useSWRConfig();
236+
const [lastDataLoadTime, setLastDataLoadTime] = React.useState<number>();
237+
const [now, setNow] = React.useState(Date.now());
238+
const [isRefreshingData, setIsRefreshingData] = React.useState(false);
239+
240+
React.useEffect(() => {
241+
const handleDashboardDataLoaded = () => {
242+
setLastDataLoadTime(Date.now());
243+
};
244+
245+
window.addEventListener(
246+
DASHBOARD_DATA_LOADED_EVENT,
247+
handleDashboardDataLoaded,
248+
);
249+
250+
return () => {
251+
window.removeEventListener(
252+
DASHBOARD_DATA_LOADED_EVENT,
253+
handleDashboardDataLoaded,
254+
);
255+
};
256+
}, []);
257+
258+
React.useEffect(() => {
259+
const interval = window.setInterval(() => {
260+
setNow(Date.now());
261+
}, 5000);
262+
263+
return () => {
264+
window.clearInterval(interval);
265+
};
266+
}, []);
267+
268+
const refreshDashboardData = async () => {
269+
setIsRefreshingData(true);
270+
try {
271+
await mutate(() => true);
272+
} finally {
273+
setIsRefreshingData(false);
274+
}
275+
};
276+
277+
return (
278+
<Box
279+
sx={{
280+
alignItems: "center",
281+
display: "flex",
282+
flexShrink: 0,
283+
gap: 1,
284+
marginRight: 1,
285+
}}
286+
>
287+
<Tooltip
288+
title={
289+
lastDataLoadTime
290+
? `Last successful data load: ${formatLastDataLoadTime(
291+
lastDataLoadTime,
292+
currentTimeZone,
293+
)}`
294+
: "No dashboard data has loaded yet."
295+
}
296+
>
297+
<Typography
298+
color="text.secondary"
299+
sx={{ whiteSpace: "nowrap" }}
300+
variant="caption"
301+
>
302+
{formatFreshnessLabel(lastDataLoadTime, now)}
303+
</Typography>
304+
</Tooltip>
305+
<Tooltip title="Refresh dashboard data">
306+
<span>
307+
<IconButton
308+
aria-label="Refresh dashboard data"
309+
disabled={isRefreshingData}
310+
onClick={refreshDashboardData}
311+
size="large"
312+
sx={(theme) => ({ color: theme.palette.text.secondary })}
313+
>
314+
<RiRefreshLine />
315+
</IconButton>
316+
</span>
317+
</Tooltip>
318+
</Box>
319+
);
320+
};
321+
322+
export const formatLastDataLoadTime = (
323+
lastDataLoadTime: number,
324+
currentTimeZone: string | undefined,
325+
) => {
326+
return new Date(lastDataLoadTime).toLocaleString(
327+
undefined,
328+
currentTimeZone ? { timeZone: currentTimeZone } : undefined,
329+
);
330+
};
331+
332+
export const formatFreshnessLabel = (
333+
lastDataLoadTime: number | undefined,
334+
now: number,
335+
) => {
336+
if (!lastDataLoadTime) {
337+
return "No data loaded";
338+
}
339+
340+
const ageMs = Math.max(0, now - lastDataLoadTime);
341+
const fiveSecondsMs = 5 * 1000;
342+
const minuteMs = 60 * 1000;
343+
const hourMs = 60 * minuteMs;
344+
const dayMs = 24 * hourMs;
345+
346+
if (ageMs < fiveSecondsMs) {
347+
return "Updated just now";
348+
}
349+
350+
if (ageMs < minuteMs) {
351+
const ageSeconds = Math.min(55, Math.round(ageMs / fiveSecondsMs) * 5);
352+
return `Updated ${ageSeconds}s ago`;
353+
}
354+
355+
if (ageMs < hourMs) {
356+
return `Updated ${Math.floor(ageMs / minuteMs)}m ago`;
357+
}
358+
359+
if (ageMs < dayMs) {
360+
return `Updated ${Math.floor(ageMs / hourMs)}h ago`;
361+
}
362+
363+
return `Updated ${Math.floor(ageMs / dayMs)}d ago`;
364+
};
365+
226366
const MainNavBreadcrumbs = () => {
227367
const { mainNavPageHierarchy } = useContext(MainNavContext);
228368

python/ray/dashboard/client/src/service/requestHandlers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
1212
import { AUTHENTICATION_ERROR_EVENT } from "../authentication/constants";
13+
import { DASHBOARD_DATA_LOADED_EVENT } from "../common/constants";
1314

1415
/**
1516
* This function formats URLs such that the user's browser
@@ -36,6 +37,7 @@ export { axiosInstance };
3637
// Response interceptor: Handle 401/403 errors
3738
axiosInstance.interceptors.response.use(
3839
(response) => {
40+
window.dispatchEvent(new Event(DASHBOARD_DATA_LOADED_EVENT));
3941
return response;
4042
},
4143
(error) => {

python/ray/data/BUILD.bazel

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1738,6 +1738,20 @@ py_test(
17381738
],
17391739
)
17401740

1741+
py_test(
1742+
name = "test_usage",
1743+
size = "small",
1744+
srcs = ["tests/test_usage.py"],
1745+
tags = [
1746+
"exclusive",
1747+
"team:data",
1748+
],
1749+
deps = [
1750+
":conftest",
1751+
"//:ray_lib",
1752+
],
1753+
)
1754+
17411755
py_test(
17421756
name = "test_util",
17431757
size = "small",

0 commit comments

Comments
 (0)