diff --git a/plugins/plugin-codeflare/src/components/ProfileExplorer.tsx b/plugins/plugin-codeflare/src/components/ProfileExplorer.tsx index fc014df9..fcd3e3ac 100644 --- a/plugins/plugin-codeflare/src/components/ProfileExplorer.tsx +++ b/plugins/plugin-codeflare/src/components/ProfileExplorer.tsx @@ -33,11 +33,16 @@ import { DescriptionListTerm, DescriptionListDescription, Divider, + Select, + SelectVariant, + SelectOption, } from "@patternfly/react-core" import ProfileSelect from "./ProfileSelect" import DashboardSelect from "./DashboardSelect" import ProfileWatcher from "../tray/watchers/profile/list" +import ProfileStatusWatcher from "../tray/watchers/profile/status" +import UpdateFunction from "../tray/update" import { handleBoot, handleShutdown } from "../controller/profile/actions" import "../../web/scss/components/Dashboard/Description.scss" @@ -49,9 +54,12 @@ type Props = { type State = { watcher: ProfileWatcher + statusWatcher: ProfileStatusWatcher selectedProfile?: string profiles?: Profiles.Profile[] catastrophicError?: unknown + + updateCount: number } export default class ProfileExplorer extends React.PureComponent { @@ -60,6 +68,12 @@ export default class ProfileExplorer extends React.PureComponent { this.init() } + private readonly statusWatcherUpdateFn: UpdateFunction = () => { + this.setState((curState) => ({ + updateCount: (curState?.updateCount || 0) + 1, + })) + } + private readonly _handleProfileSelection = (selectedProfile: string) => { this.setState({ selectedProfile }) @@ -70,7 +84,7 @@ export default class ProfileExplorer extends React.PureComponent { private updateDebouncer: null | ReturnType = null - private readonly updateFn = () => { + private readonly profileWatcherUpdateFn = () => { if (this.updateDebouncer) { clearTimeout(this.updateDebouncer) } @@ -117,7 +131,10 @@ export default class ProfileExplorer extends React.PureComponent { private async init() { try { - const watcher = await new ProfileWatcher(this.updateFn, await Profiles.profilesPath({}, true)).init() + const watcher = await new ProfileWatcher( + this.profileWatcherUpdateFn, + await Profiles.profilesPath({}, true) + ).init() this.setState({ watcher, profiles: [], @@ -129,13 +146,19 @@ export default class ProfileExplorer extends React.PureComponent { } public componentWillUnmount() { - if (this.state && this.state.watcher) { - this.state.watcher.close() + this.state?.watcher?.close() + } + + public componentDidUpdate(prevProps: Props, prevState: State) { + if (prevState?.selectedProfile !== this.state?.selectedProfile) { + if (!this.state?.selectedProfile) return + const statusWatcher = new ProfileStatusWatcher(this.state.selectedProfile, this.statusWatcherUpdateFn) + this.setState({ statusWatcher }) } } public render() { - if (this.state && this.state.catastrophicError) { + if (this.state?.catastrophicError) { return "Internal Error" } else if (!this.state || !this.state.profiles || !this.state.selectedProfile) { return @@ -146,6 +169,8 @@ export default class ProfileExplorer extends React.PureComponent { profile={this.state.selectedProfile} profiles={this.state.profiles} onSelectProfile={this._handleProfileSelection} + profileReadiness={this.state.statusWatcher?.readiness} + profileStatus={this.state.statusWatcher} /> ) @@ -153,13 +178,29 @@ export default class ProfileExplorer extends React.PureComponent { } } -class ProfileCard extends React.PureComponent<{ +type ProfileCardProps = { profile: string profiles: Profiles.Profile[] onSelectProfile: (profile: string) => void -}> { + + profileReadiness: string + profileStatus: ProfileStatusWatcher +} + +type ProfileCardState = { + isOpen: boolean +} + +class ProfileCard extends React.PureComponent { + public constructor(props: ProfileCardProps) { + super(props) + this.state = { + isOpen: false, + } + } private readonly _handleBoot = () => handleBoot(this.props.profile) private readonly _handleShutdown = () => handleShutdown(this.props.profile) + private readonly _onToggle = () => this.setState({ isOpen: !this.state.isOpen }) private title() { return ( @@ -174,7 +215,28 @@ class ProfileCard extends React.PureComponent<{ } private actions() { - return "Status: pending" + const StatusTitle = ({ readiness }: { readiness: string | undefined }) => ( + + Status +
+
+ ) + return ( + + ) } private body() { diff --git a/plugins/plugin-codeflare/src/tray/watchers/profile/status.ts b/plugins/plugin-codeflare/src/tray/watchers/profile/status.ts index 19758d5b..3647b57e 100644 --- a/plugins/plugin-codeflare/src/tray/watchers/profile/status.ts +++ b/plugins/plugin-codeflare/src/tray/watchers/profile/status.ts @@ -36,6 +36,21 @@ export default class ProfileStatusWatcher { /* this._job = */ this.initJob(profile) } + public get readiness() { + return this.headReadiness === "pending" || this.workerReadiness === "pending" + ? "pending" + : this.headReadiness === "error" || this.workerReadiness === "error" + ? "error" + : !this.isReady(this.headReadiness) && !this.isReady(this.workerReadiness) + ? "pending" + : "success" + } + + private isReady(readiness: string) { + const match = readiness.match(/^(\d)+\/(\d)+$/) + return match && match[1] === match[2] + } + public get head() { return { label: `Head nodes: ${this.headReadiness}` } } @@ -102,12 +117,15 @@ export default class ProfileStatusWatcher { }) job.stdout.on("data", (data) => { + const headBefore = this.headReadiness + const workersBefore = this.workerReadiness + data .toString() .split(/\n/) .forEach((line: string) => { - Debug("codeflare")("profile status watcher line", line) const match = line.match(/^(head|workers)\s+(\S+)$/) + Debug("codeflare")("profile status watcher line", this.profile, line, match) if (!match) { // console.error('Bogus line emitted by ray cluster readiness probe', line) } else { @@ -121,7 +139,10 @@ export default class ProfileStatusWatcher { } }) - this.updateFunction() + if (this.headReadiness !== headBefore || this.workerReadiness !== workersBefore) { + Debug("codeflare")("profile status watcher change", this.profile) + this.updateFunction() + } }) return job diff --git a/plugins/plugin-codeflare/web/scss/components/ProfileExplorer/_index.scss b/plugins/plugin-codeflare/web/scss/components/ProfileExplorer/_index.scss index 521c6f7f..b887a229 100644 --- a/plugins/plugin-codeflare/web/scss/components/ProfileExplorer/_index.scss +++ b/plugins/plugin-codeflare/web/scss/components/ProfileExplorer/_index.scss @@ -14,9 +14,34 @@ * limitations under the License. */ -.codeflare--profile-explorer { +@mixin ProfileExplorer { + .codeflare--profile-explorer { + @content; + } +} + +@mixin ProfileStatus { + .codeflare--profile-explorer--select-status { + @content; + } +} + +@include ProfileExplorer { font-family: var(--font-sans-serif); + .pf-c-card { + --pf-c-card--BackgroundColor: var(--color-base00); + } + + .pf-c-select { + --pf-c-select__toggle--BackgroundColor: var(--color-base00); + + button, + .pf-c-select__toggle-text { + color: var(--color-text-01) !important; + } + } + hr.pf-c-divider { margin: 0; border: none; @@ -25,4 +50,25 @@ overflow: hidden; text-overflow: ellipsis; } + + &--status-light { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; + margin-left: 10px; + background-color: var(--color-gray); + + &--error { + background-color: var(--color-error); + } + + &--pending { + background-color: var(--color-warning); + } + + &--success { + background-color: var(--color-ok); + } + } }