diff --git a/python/ray/dashboard/client/src/api.ts b/python/ray/dashboard/client/src/api.ts index 91f35e4f9..73553669f 100644 --- a/python/ray/dashboard/client/src/api.ts +++ b/python/ray/dashboard/client/src/api.ts @@ -109,6 +109,38 @@ export type NodeInfoResponse = { export const getNodeInfo = () => get("/api/node_info", {}); +export type RayletActorInfo = + | { + actorId: string; + actorTitle: string; + averageTaskExecutionSpeed: number; + children: RayletInfoResponse["actors"]; + // currentTaskFuncDesc: string[]; + ipAddress: string; + jobId: string; + nodeId: string; + numExecutedTasks: number; + numLocalObjects: number; + numObjectIdsInScope: number; + pid: number; + port: number; + state: 0 | 1 | 2; + taskQueueLength: number; + timestamp: number; + usedObjectStoreMemory: number; + usedResources: { [key: string]: number }; + currentTaskDesc?: string; + numPendingTasks?: number; + webuiDisplay?: Record; + } + | { + actorId: string; + actorTitle: string; + requiredResources: { [key: string]: number }; + state: -1; + invalidStateType?: "infeasibleActor" | "pendingActor"; + }; + export type RayletInfoResponse = { nodes: { [ip: string]: { @@ -120,37 +152,7 @@ export type RayletInfoResponse = { }; }; actors: { - [actorId: string]: - | { - actorId: string; - actorTitle: string; - averageTaskExecutionSpeed: number; - children: RayletInfoResponse["actors"]; - // currentTaskFuncDesc: string[]; - ipAddress: string; - jobId: string; - nodeId: string; - numExecutedTasks: number; - numLocalObjects: number; - numObjectIdsInScope: number; - pid: number; - port: number; - state: 0 | 1 | 2; - taskQueueLength: number; - timestamp: number; - usedObjectStoreMemory: number; - usedResources: { [key: string]: number }; - currentTaskDesc?: string; - numPendingTasks?: number; - webuiDisplay?: Record; - } - | { - actorId: string; - actorTitle: string; - requiredResources: { [key: string]: number }; - state: -1; - invalidStateType?: "infeasibleActor" | "pendingActor"; - }; + [actorId: string]: RayletActorInfo; }; }; diff --git a/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx index a607a3d98..46d161b51 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actor.tsx @@ -13,7 +13,7 @@ import { getProfilingResultURL, launchKillActor, launchProfiling, - RayletInfoResponse, + RayletActorInfo, } from "../../../api"; import Actors from "./Actors"; @@ -61,7 +61,7 @@ const styles = (theme: Theme) => }); type Props = { - actor: RayletInfoResponse["actors"][keyof RayletInfoResponse["actors"]]; + actor: RayletActorInfo; }; type State = { diff --git a/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx index 5e20f7a72..c8b2652c5 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/Actors.tsx @@ -1,21 +1,18 @@ -import { createStyles, Theme, withStyles, WithStyles } from "@material-ui/core"; -import React from "react"; +import React, { Fragment } from "react"; import { RayletInfoResponse } from "../../../api"; import Actor from "./Actor"; -const styles = (theme: Theme) => createStyles({}); - -type Props = { +type ActorProps = { actors: RayletInfoResponse["actors"]; }; -class Actors extends React.Component> { - render() { - const { actors } = this.props; - return Object.entries(actors).map(([actorId, actor]) => ( - - )); - } -} +const Actors = (props: ActorProps) => { + const { actors } = props; -export default withStyles(styles)(Actors); + const actorChildren = Object.entries(actors).map(([actorId, actor]) => ( + + )); + return {actorChildren}; +}; + +export default Actors; diff --git a/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx b/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx index 6ebb6baf4..f7d45a7ae 100644 --- a/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx +++ b/python/ray/dashboard/client/src/pages/dashboard/logical-view/LogicalView.tsx @@ -1,48 +1,96 @@ import { - createStyles, - Theme, + FormControl, + FormHelperText, + Input, + InputLabel, Typography, - WithStyles, - withStyles, } from "@material-ui/core"; -import React from "react"; +import React, { useState } from "react"; import { connect } from "react-redux"; +import { RayletActorInfo, RayletInfoResponse } from "../../../api"; import { StoreState } from "../../../store"; import Actors from "./Actors"; -const styles = (theme: Theme) => - createStyles({ - warning: { - fontSize: "0.8125rem", - marginBottom: theme.spacing(2), - }, - warningIcon: { - fontSize: "1.25em", - verticalAlign: "text-bottom", - }, - }); +const actorMatchesSearch = ( + actor: RayletActorInfo, + nameFilter: string, +): boolean => { + // Performs a case insensitive search for the name filter string within the + // actor and all of its nested subactors. + const actorTitles = getNestedActorTitles(actor); + const loweredNameFilter = nameFilter.toLowerCase(); + const match = actorTitles.find( + (actorTitle) => actorTitle.toLowerCase().search(loweredNameFilter) !== -1, + ); + return match !== undefined; +}; + +const getNestedActorTitles = (actor: RayletActorInfo): string[] => { + const actorTitle = actor.actorTitle; + const titles: string[] = actorTitle ? [actorTitle] : []; + // state of -1 indicates an actor data record that does not have children. + if (actor.state === -1) { + return titles; + } + const children = actor["children"]; + if (children === undefined || Object.entries(children).length === 0) { + return titles; + } + const childrenTitles = Object.values(children).flatMap((actor) => + getNestedActorTitles(actor), + ); + return titles.concat(childrenTitles); +}; const mapStateToProps = (state: StoreState) => ({ rayletInfo: state.dashboard.rayletInfo, }); -class LogicalView extends React.Component< - WithStyles & ReturnType -> { - render() { - const { rayletInfo } = this.props; - return ( -
- {rayletInfo === null ? ( - Loading... - ) : Object.entries(rayletInfo.actors).length === 0 ? ( - No actors found. - ) : ( - - )} -
+const filterObj = (obj: Object, filterFn: any) => + Object.fromEntries(Object.entries(obj).filter(filterFn)); + +type LogicalViewProps = { + rayletInfo: RayletInfoResponse | null; +} & ReturnType; + +const LogicalView = ({ rayletInfo }: LogicalViewProps) => { + const [nameFilter, setNameFilter] = useState(""); + + if (rayletInfo === null) { + return Loading...; + } + let filteredActors = rayletInfo.actors; + if (nameFilter !== "") { + filteredActors = filterObj( + filteredActors, + ([_, actor]: [any, RayletActorInfo]) => + actorMatchesSearch(actor, nameFilter), ); } -} -export default connect(mapStateToProps)(withStyles(styles)(LogicalView)); + return ( +
+ {Object.entries(rayletInfo.actors).length === 0 ? ( + No actors found. + ) : ( +
+ + Actor Search + setNameFilter(event.target.value)} + /> + + Search for an actor by name + + + +
+ )} +
+ ); +}; + +export default connect(mapStateToProps)(LogicalView);