Skip to content

Commit 534c94f

Browse files
Pablo Carmonastarpit
Pablo Carmona
authored andcommitted
feat: add status dropdown with watcher support
1 parent ee16c63 commit 534c94f

File tree

3 files changed

+140
-11
lines changed

3 files changed

+140
-11
lines changed

Diff for: plugins/plugin-codeflare/src/components/ProfileExplorer.tsx

+70-8
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ import {
3333
DescriptionListTerm,
3434
DescriptionListDescription,
3535
Divider,
36+
Select,
37+
SelectVariant,
38+
SelectOption,
3639
} from "@patternfly/react-core"
3740

3841
import ProfileSelect from "./ProfileSelect"
3942
import DashboardSelect from "./DashboardSelect"
4043
import ProfileWatcher from "../tray/watchers/profile/list"
44+
import ProfileStatusWatcher from "../tray/watchers/profile/status"
45+
import UpdateFunction from "../tray/update"
4146
import { handleBoot, handleShutdown } from "../controller/profile/actions"
4247

4348
import "../../web/scss/components/Dashboard/Description.scss"
@@ -49,9 +54,12 @@ type Props = {
4954

5055
type State = {
5156
watcher: ProfileWatcher
57+
statusWatcher: ProfileStatusWatcher
5258
selectedProfile?: string
5359
profiles?: Profiles.Profile[]
5460
catastrophicError?: unknown
61+
62+
updateCount: number
5563
}
5664

5765
export default class ProfileExplorer extends React.PureComponent<Props, State> {
@@ -60,6 +68,12 @@ export default class ProfileExplorer extends React.PureComponent<Props, State> {
6068
this.init()
6169
}
6270

71+
private readonly statusWatcherUpdateFn: UpdateFunction = () => {
72+
this.setState((curState) => ({
73+
updateCount: (curState?.updateCount || 0) + 1,
74+
}))
75+
}
76+
6377
private readonly _handleProfileSelection = (selectedProfile: string) => {
6478
this.setState({ selectedProfile })
6579

@@ -70,7 +84,7 @@ export default class ProfileExplorer extends React.PureComponent<Props, State> {
7084

7185
private updateDebouncer: null | ReturnType<typeof setTimeout> = null
7286

73-
private readonly updateFn = () => {
87+
private readonly profileWatcherUpdateFn = () => {
7488
if (this.updateDebouncer) {
7589
clearTimeout(this.updateDebouncer)
7690
}
@@ -117,7 +131,10 @@ export default class ProfileExplorer extends React.PureComponent<Props, State> {
117131

118132
private async init() {
119133
try {
120-
const watcher = await new ProfileWatcher(this.updateFn, await Profiles.profilesPath({}, true)).init()
134+
const watcher = await new ProfileWatcher(
135+
this.profileWatcherUpdateFn,
136+
await Profiles.profilesPath({}, true)
137+
).init()
121138
this.setState({
122139
watcher,
123140
profiles: [],
@@ -129,13 +146,19 @@ export default class ProfileExplorer extends React.PureComponent<Props, State> {
129146
}
130147

131148
public componentWillUnmount() {
132-
if (this.state && this.state.watcher) {
133-
this.state.watcher.close()
149+
this.state?.watcher?.close()
150+
}
151+
152+
public componentDidUpdate(prevProps: Props, prevState: State) {
153+
if (prevState?.selectedProfile !== this.state?.selectedProfile) {
154+
if (!this.state?.selectedProfile) return
155+
const statusWatcher = new ProfileStatusWatcher(this.state.selectedProfile, this.statusWatcherUpdateFn)
156+
this.setState({ statusWatcher })
134157
}
135158
}
136159

137160
public render() {
138-
if (this.state && this.state.catastrophicError) {
161+
if (this.state?.catastrophicError) {
139162
return "Internal Error"
140163
} else if (!this.state || !this.state.profiles || !this.state.selectedProfile) {
141164
return <Loading />
@@ -146,20 +169,38 @@ export default class ProfileExplorer extends React.PureComponent<Props, State> {
146169
profile={this.state.selectedProfile}
147170
profiles={this.state.profiles}
148171
onSelectProfile={this._handleProfileSelection}
172+
profileReadiness={this.state.statusWatcher?.readiness}
173+
profileStatus={this.state.statusWatcher}
149174
/>
150175
</div>
151176
)
152177
}
153178
}
154179
}
155180

156-
class ProfileCard extends React.PureComponent<{
181+
type ProfileCardProps = {
157182
profile: string
158183
profiles: Profiles.Profile[]
159184
onSelectProfile: (profile: string) => void
160-
}> {
185+
186+
profileReadiness: string
187+
profileStatus: ProfileStatusWatcher
188+
}
189+
190+
type ProfileCardState = {
191+
isOpen: boolean
192+
}
193+
194+
class ProfileCard extends React.PureComponent<ProfileCardProps, ProfileCardState> {
195+
public constructor(props: ProfileCardProps) {
196+
super(props)
197+
this.state = {
198+
isOpen: false,
199+
}
200+
}
161201
private readonly _handleBoot = () => handleBoot(this.props.profile)
162202
private readonly _handleShutdown = () => handleShutdown(this.props.profile)
203+
private readonly _onToggle = () => this.setState({ isOpen: !this.state.isOpen })
163204

164205
private title() {
165206
return (
@@ -174,7 +215,28 @@ class ProfileCard extends React.PureComponent<{
174215
}
175216

176217
private actions() {
177-
return "Status: pending"
218+
const StatusTitle = ({ readiness }: { readiness: string | undefined }) => (
219+
<React.Fragment>
220+
<span>Status</span>
221+
<div
222+
className={`codeflare--profile-explorer--status-light codeflare--profile-explorer--status-light--${readiness}`}
223+
></div>
224+
</React.Fragment>
225+
)
226+
return (
227+
<Select
228+
className="codeflare--profile-explorer--select-status"
229+
variant={SelectVariant.single}
230+
placeholderText={<StatusTitle readiness={this.props.profileStatus?.readiness} />}
231+
label="Status select"
232+
onToggle={this._onToggle}
233+
isOpen={this.state.isOpen}
234+
aria-labelledby="select-status-label"
235+
>
236+
<SelectOption isPlaceholder>{this.props.profileStatus?.head.label}</SelectOption>
237+
<SelectOption isPlaceholder>{this.props.profileStatus?.workers.label}</SelectOption>
238+
</Select>
239+
)
178240
}
179241

180242
private body() {

Diff for: plugins/plugin-codeflare/src/tray/watchers/profile/status.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ export default class ProfileStatusWatcher {
3636
/* this._job = */ this.initJob(profile)
3737
}
3838

39+
public get readiness() {
40+
return this.headReadiness === "pending" || this.workerReadiness === "pending"
41+
? "pending"
42+
: this.headReadiness === "error" || this.workerReadiness === "error"
43+
? "error"
44+
: !this.isReady(this.headReadiness) && !this.isReady(this.workerReadiness)
45+
? "pending"
46+
: "success"
47+
}
48+
49+
private isReady(readiness: string) {
50+
const match = readiness.match(/^(\d)+\/(\d)+$/)
51+
return match && match[1] === match[2]
52+
}
53+
3954
public get head() {
4055
return { label: `Head nodes: ${this.headReadiness}` }
4156
}
@@ -102,12 +117,15 @@ export default class ProfileStatusWatcher {
102117
})
103118

104119
job.stdout.on("data", (data) => {
120+
const headBefore = this.headReadiness
121+
const workersBefore = this.workerReadiness
122+
105123
data
106124
.toString()
107125
.split(/\n/)
108126
.forEach((line: string) => {
109-
Debug("codeflare")("profile status watcher line", line)
110127
const match = line.match(/^(head|workers)\s+(\S+)$/)
128+
Debug("codeflare")("profile status watcher line", this.profile, line, match)
111129
if (!match) {
112130
// console.error('Bogus line emitted by ray cluster readiness probe', line)
113131
} else {
@@ -121,7 +139,10 @@ export default class ProfileStatusWatcher {
121139
}
122140
})
123141

124-
this.updateFunction()
142+
if (this.headReadiness !== headBefore || this.workerReadiness !== workersBefore) {
143+
Debug("codeflare")("profile status watcher change", this.profile)
144+
this.updateFunction()
145+
}
125146
})
126147

127148
return job

Diff for: plugins/plugin-codeflare/web/scss/components/ProfileExplorer/_index.scss

+47-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,34 @@
1414
* limitations under the License.
1515
*/
1616

17-
.codeflare--profile-explorer {
17+
@mixin ProfileExplorer {
18+
.codeflare--profile-explorer {
19+
@content;
20+
}
21+
}
22+
23+
@mixin ProfileStatus {
24+
.codeflare--profile-explorer--select-status {
25+
@content;
26+
}
27+
}
28+
29+
@include ProfileExplorer {
1830
font-family: var(--font-sans-serif);
1931

32+
.pf-c-card {
33+
--pf-c-card--BackgroundColor: var(--color-base00);
34+
}
35+
36+
.pf-c-select {
37+
--pf-c-select__toggle--BackgroundColor: var(--color-base00);
38+
39+
button,
40+
.pf-c-select__toggle-text {
41+
color: var(--color-text-01) !important;
42+
}
43+
}
44+
2045
hr.pf-c-divider {
2146
margin: 0;
2247
border: none;
@@ -25,4 +50,25 @@
2550
overflow: hidden;
2651
text-overflow: ellipsis;
2752
}
53+
54+
&--status-light {
55+
width: 10px;
56+
height: 10px;
57+
border-radius: 50%;
58+
display: inline-block;
59+
margin-left: 10px;
60+
background-color: var(--color-gray);
61+
62+
&--error {
63+
background-color: var(--color-error);
64+
}
65+
66+
&--pending {
67+
background-color: var(--color-warning);
68+
}
69+
70+
&--success {
71+
background-color: var(--color-ok);
72+
}
73+
}
2874
}

0 commit comments

Comments
 (0)