Skip to content

Commit a82be8e

Browse files
committed
chore: refactor WorkloadDesigner into its own file
1 parent 04b72a8 commit a82be8e

File tree

5 files changed

+374
-303
lines changed

5 files changed

+374
-303
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2022 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from "react"
18+
import { Allotment } from "allotment"
19+
20+
export default class AllotmentFillPane extends React.PureComponent<{ minSize?: number }> {
21+
public render() {
22+
return (
23+
<Allotment.Pane className="flex-fill flex-layout flex-align-stretch" minSize={this.props.minSize}>
24+
{this.props.children}
25+
</Allotment.Pane>
26+
)
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2022 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from "react"
18+
import { Button, EmptyState, EmptyStateBody, EmptyStatePrimary, Flex, FlexItem, Title } from "@patternfly/react-core"
19+
20+
export default class Empty extends React.PureComponent<{ refresh(): void; gotit(): void }> {
21+
/** Run through all questions again */
22+
private resubmit() {
23+
return (
24+
<Flex>
25+
<FlexItem>
26+
<Button variant="secondary" onClick={this.props.gotit}>
27+
Got it!
28+
</Button>
29+
</FlexItem>
30+
<FlexItem>
31+
<Button variant="tertiary" onClick={this.props.refresh}>
32+
Walk through the constraints again
33+
</Button>
34+
</FlexItem>
35+
</Flex>
36+
)
37+
}
38+
39+
public render() {
40+
return (
41+
<EmptyState variant="xs" className="sans-serif flex-fill codeflare--workload-comparo">
42+
<Title size="lg" headingLevel="h4">
43+
All constraints satisfied
44+
</Title>
45+
<EmptyStateBody>
46+
All application, compute, and storage constraints have been defined by your Draft Specification.
47+
</EmptyStateBody>
48+
<EmptyStatePrimary>{this.resubmit()}</EmptyStatePrimary>
49+
</EmptyState>
50+
)
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/*
2+
* Copyright 2022 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from "react"
18+
import { Allotment, AllotmentHandle } from "allotment"
19+
import { Loading } from "@kui-shell/plugin-client-common"
20+
import { encodeComponent } from "@kui-shell/core"
21+
import { defaultGuidebook as defaultGuidebookFromClient } from "@kui-shell/client/config.d/client.json"
22+
23+
import respawn from "../controller/respawn"
24+
25+
import NoGuidebook from "./NoGuidebook"
26+
import AskingTerminal from "./AskingTerminal"
27+
import ProfileExplorer from "./ProfileExplorer"
28+
import AllotmentFillPane from "./AllotmentFillPane"
29+
import { Props as BaseProps } from "./RestartableTerminal"
30+
31+
import "../../web/scss/components/Allotment/_index.scss"
32+
import "allotment/dist/style.css"
33+
34+
/**
35+
* ProfileExplorer | props.aboveTerminal?
36+
* | ----------
37+
* | Ask \
38+
* | ---------- -- AskingTerminal on the right
39+
* | Terminal /
40+
*/
41+
export default class WorkloadDesigner extends React.PureComponent<Props, State> {
42+
/** Allotment initial split ... allotments */
43+
private readonly splits = {
44+
horizontal: [25, 75],
45+
vertical1: [50, 50], // no `this.props.aboveTerminal`
46+
vertical2a: [60, 40], // yes, and show a guidebook
47+
vertical2b: [80, 20], // yes, and do not show a guidebook
48+
}
49+
50+
private readonly tasks = [{ label: "Run a Job", argv: ["codeflare", "-p", "${SELECTED_PROFILE}"] }]
51+
52+
public constructor(props: Props) {
53+
super(props)
54+
55+
this.state = {
56+
initCount: 0,
57+
guidebook: props.defaultGuidebook === null ? null : props.defaultGuidebook || defaultGuidebookFromClient,
58+
}
59+
this.init()
60+
}
61+
62+
/**
63+
* Initialize for a new guidebook execution. Which guidebook depends
64+
* on: if as given, then as given in props, then as given in
65+
* client.
66+
*/
67+
private async init() {
68+
const guidebook = this.state.guidebook
69+
70+
if (guidebook === null) {
71+
return
72+
}
73+
74+
try {
75+
// respawn, meaning launch it with codeflare
76+
const { argv, env } = await respawn(this.tasks[0].argv)
77+
const cmdline = [
78+
...argv.map((_) => encodeComponent(_)),
79+
guidebook,
80+
...(this.state.noninteractive ? ["--y"] : []),
81+
...(this.state.ifor ? ["--ifor", guidebook] : []),
82+
]
83+
.filter(Boolean)
84+
.join(" ")
85+
86+
this.setState((curState) => ({
87+
cmdline,
88+
hideTerminal: false,
89+
initCount: curState.initCount + 1,
90+
env: Object.assign(
91+
{},
92+
env,
93+
{
94+
/* MWCLEAR_INITIAL: "true" */
95+
},
96+
this.state.extraEnv
97+
),
98+
}))
99+
} catch (error) {
100+
console.error("Error initializing command line", error)
101+
this.setState((curState) => ({
102+
error: true,
103+
initCount: curState.initCount + 1,
104+
}))
105+
}
106+
}
107+
108+
/** Event handler for switching to a different profile */
109+
private readonly onSelectProfile = (selectedProfile: string, profiles?: import("madwizard").Profiles.Profile[]) => {
110+
this.setState({ selectedProfile })
111+
112+
if (this.props.onSelectProfile) {
113+
this.props.onSelectProfile(selectedProfile, profiles)
114+
}
115+
}
116+
117+
/** Event handler for switching to a different guidebook */
118+
private readonly onSelectGuidebook = (guidebook: string | null | undefined, ifor = true) =>
119+
this.setState({ hideTerminal: false, guidebook, ifor, noninteractive: false })
120+
121+
public static getDerivedStateFromProps(props: Props, state: State) {
122+
if ((props.defaultGuidebook && state.guidebook !== props.defaultGuidebook) || props.extraEnv !== state.extraEnv) {
123+
// different guidebook or different env vars to be passed to that guidebook
124+
return {
125+
ifor: false,
126+
extraEnv: props.extraEnv,
127+
guidebook: props.defaultGuidebook,
128+
noninteractive: props.defaultNoninteractive,
129+
}
130+
} else if (props.defaultNoninteractive !== undefined && props.defaultNoninteractive !== state.noninteractive) {
131+
// different interactivity
132+
return {
133+
ifor: false,
134+
noninteractive: props.defaultNoninteractive,
135+
}
136+
}
137+
138+
return state
139+
}
140+
141+
public static getDerivedStateFromError() {
142+
return { error: true }
143+
}
144+
public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
145+
console.error("catastrophic error", error, errorInfo)
146+
}
147+
148+
public componentDidUpdate(prevProps: Props, prevState: State) {
149+
if (prevState.guidebook !== this.state.guidebook || prevState.ifor !== this.state.ifor) {
150+
if (prevState.guidebook === null) {
151+
this.allotmentRef.current?.reset()
152+
}
153+
this.init()
154+
}
155+
}
156+
157+
private readonly _gotit = () => this.setState({ hideTerminal: true })
158+
159+
private readonly _refresh = () =>
160+
this.setState({ hideTerminal: false, guidebook: this.props.defaultGuidebook || defaultGuidebookFromClient })
161+
162+
/** Return to top-level guidebook */
163+
private readonly _home = (noninteractive = false) => {
164+
const home = this.props.defaultGuidebook || defaultGuidebookFromClient
165+
this.onSelectGuidebook(home, false)
166+
this.setState((curState) => ({ initCount: curState.initCount + 1, noninteractive }))
167+
}
168+
169+
private get vertical1() {
170+
return this.splits.vertical1
171+
}
172+
173+
private get vertical2() {
174+
return !this.state.cmdline || !this.state.env ? this.splits.vertical2b : this.splits.vertical2a
175+
}
176+
177+
private noGuidebook() {
178+
return <NoGuidebook refresh={this._refresh} gotit={this._gotit} />
179+
}
180+
181+
private readonly allotmentRef = React.createRef<AllotmentHandle>()
182+
183+
private left() {
184+
return <ProfileExplorer onSelectProfile={this.onSelectProfile} onSelectGuidebook={this.onSelectGuidebook} />
185+
}
186+
187+
private rightTop() {
188+
return this.props.aboveTerminal
189+
}
190+
191+
private rightBottom(selectedProfile: string, guidebook: string) {
192+
return !this.state.cmdline || !this.state.env ? (
193+
this.noGuidebook()
194+
) : (
195+
<AskingTerminal
196+
initCount={this.state.initCount}
197+
guidebook={guidebook}
198+
cmdline={this.state.cmdline}
199+
env={this.state.env}
200+
selectedProfile={selectedProfile}
201+
terminalProps={this.props}
202+
home={this._home}
203+
noninteractive={this.state.noninteractive}
204+
/>
205+
)
206+
}
207+
208+
private right() {
209+
const { aboveTerminal } = this.props
210+
const { hideTerminal, selectedProfile, guidebook } = this.state
211+
212+
if (!selectedProfile || !guidebook) {
213+
return <Loading />
214+
} else {
215+
return (
216+
<Allotment
217+
snap
218+
vertical
219+
defaultSizes={hideTerminal || !aboveTerminal ? this.vertical1 : this.vertical2}
220+
ref={this.allotmentRef}
221+
>
222+
{aboveTerminal && <AllotmentFillPane>{this.rightTop()}</AllotmentFillPane>}
223+
{!hideTerminal && <AllotmentFillPane>{this.rightBottom(selectedProfile, guidebook)}</AllotmentFillPane>}
224+
</Allotment>
225+
)
226+
}
227+
}
228+
229+
public render() {
230+
if (this.state.error) {
231+
return "Internal Error"
232+
}
233+
234+
return (
235+
<Allotment snap defaultSizes={this.splits.horizontal}>
236+
<AllotmentFillPane minSize={400}>{this.left()}</AllotmentFillPane>
237+
<AllotmentFillPane>{this.right()}</AllotmentFillPane>
238+
</Allotment>
239+
)
240+
}
241+
}
242+
243+
export type Props = Pick<BaseProps, "tab" | "REPL" | "onExit" | "searchable" | "fontSizeAdjust"> & {
244+
/** Default guidebook (if not given, we will take the value from the client definition); `null` means do not show anything */
245+
defaultGuidebook?: string | null
246+
247+
/** Run guidebook in non-interactive mode? */
248+
defaultNoninteractive?: boolean
249+
250+
/** Any extra env vars to add to the guidebook execution. These will be pre-joined with the default env. */
251+
extraEnv?: BaseProps["env"]
252+
253+
/** Callback when user selects a profile */
254+
onSelectProfile?(profile: string, profiles?: import("madwizard").Profiles.Profile[]): void
255+
256+
/** Content to place above the terminal */
257+
aboveTerminal?: React.ReactNode
258+
}
259+
260+
type State = Partial<Pick<BaseProps, "cmdline" | "env">> & {
261+
/** Number of times we have called this.init() */
262+
initCount: number
263+
264+
/** Internal error in rendering */
265+
error?: boolean
266+
267+
/** Use this guidebook in the terminal execution */
268+
guidebook?: string | null
269+
270+
/** Any extra env vars to add to the guidebook execution. These will be pre-joined with the default env. */
271+
extraEnv?: BaseProps["env"]
272+
273+
/** Run guidebook in non-interactive mode? */
274+
noninteractive?: boolean
275+
276+
/** Interactive only for the given guidebook? */
277+
ifor?: boolean
278+
279+
/** Use this profile in the terminal execution */
280+
selectedProfile?: string
281+
282+
/** Hide terminal? */
283+
hideTerminal?: boolean
284+
}

0 commit comments

Comments
 (0)