Skip to content

Commit a714e81

Browse files
committed
feat: add codeflare terminal command
Plus updates to the Terminal component to support user input and resize events flowing back to the controller.
1 parent 90a4ed5 commit a714e81

File tree

6 files changed

+256
-120
lines changed

6 files changed

+256
-120
lines changed

package-lock.json

Lines changed: 94 additions & 94 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@
8686
"printWidth": 120
8787
},
8888
"devDependencies": {
89-
"@kui-shell/builder": "11.5.0-dev-20220728-192258",
90-
"@kui-shell/proxy": "11.5.0-dev-20220728-192258",
91-
"@kui-shell/react": "11.5.0-dev-20220728-192258",
92-
"@kui-shell/webpack": "11.5.0-dev-20220728-192258",
89+
"@kui-shell/builder": "11.5.0-dev-20220729-154103",
90+
"@kui-shell/proxy": "11.5.0-dev-20220729-154103",
91+
"@kui-shell/react": "11.5.0-dev-20220729-154103",
92+
"@kui-shell/webpack": "11.5.0-dev-20220729-154103",
9393
"@playwright/test": "^1.23.2",
9494
"@types/debug": "^4.1.7",
9595
"@types/node": "14.11.8",
@@ -113,16 +113,16 @@
113113
},
114114
"dependencies": {
115115
"@kui-shell/client": "file:./plugins/plugin-client-default",
116-
"@kui-shell/core": "11.5.0-dev-20220728-192258",
117-
"@kui-shell/plugin-bash-like": "11.5.0-dev-20220728-192258",
118-
"@kui-shell/plugin-client-common": "11.5.0-dev-20220728-192258",
116+
"@kui-shell/core": "11.5.0-dev-20220729-154103",
117+
"@kui-shell/plugin-bash-like": "11.5.0-dev-20220729-154103",
118+
"@kui-shell/plugin-client-common": "11.5.0-dev-20220729-154103",
119119
"@kui-shell/plugin-codeflare": "file:./plugins/plugin-codeflare",
120-
"@kui-shell/plugin-core-support": "11.5.0-dev-20220728-192258",
121-
"@kui-shell/plugin-electron-components": "11.5.0-dev-20220728-192258",
122-
"@kui-shell/plugin-kubectl": "11.5.0-dev-20220728-192258",
120+
"@kui-shell/plugin-core-support": "11.5.0-dev-20220729-154103",
121+
"@kui-shell/plugin-electron-components": "11.5.0-dev-20220729-154103",
122+
"@kui-shell/plugin-kubectl": "11.5.0-dev-20220729-154103",
123123
"@kui-shell/plugin-madwizard": "file:./plugins/plugin-madwizard",
124-
"@kui-shell/plugin-patternfly4-themes": "11.5.0-dev-20220728-192258",
125-
"@kui-shell/plugin-proxy-support": "11.5.0-dev-20220728-192258",
126-
"@kui-shell/plugin-s3": "11.5.0-dev-20220728-192258"
124+
"@kui-shell/plugin-patternfly4-themes": "11.5.0-dev-20220729-154103",
125+
"@kui-shell/plugin-proxy-support": "11.5.0-dev-20220729-154103",
126+
"@kui-shell/plugin-s3": "11.5.0-dev-20220729-154103"
127127
}
128128
}

plugins/plugin-client-default/notebooks/hello.md

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,52 @@ and machine learning pipelines on the cloud.
2121
2222
---
2323

24-
=== "See CodeFlare in Action"
24+
=== "Gallery"
25+
=== "CodeFlare CLI"
26+
```shell
27+
---
28+
execute: now
29+
outputOnly: true
30+
maximize: true
31+
---
32+
codeflare gallery
33+
```
34+
35+
=== "CodeFlare Dashboard"
36+
```shell
37+
---
38+
execute: now
39+
outputOnly: true
40+
maximize: true
41+
---
42+
codeflare dashboard-gallery
43+
```
44+
45+
=== "Your Profiles"
2546
```shell
2647
---
2748
execute: now
2849
outputOnly: true
29-
maximize: true
3050
---
31-
codeflare gallery
51+
codeflare get profile
3252
```
3353

34-
=== "See Example Dashboards"
54+
=== "Use the CodeFlare CLI"
3555
```shell
3656
---
3757
execute: now
3858
outputOnly: true
3959
maximize: true
4060
---
41-
codeflare dashboard-gallery
61+
codeflare terminal codeflare
4262
```
4363

44-
=== "Your Profiles"
45-
64+
=== "Terminal"
4665
```shell
4766
---
4867
execute: now
4968
outputOnly: true
69+
maximize: true
5070
---
51-
codeflare get profile
71+
codeflare terminal $SHELL
5272
```

plugins/plugin-codeflare/src/components/Terminal.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,28 @@ type WatchInit = () => {
3030
*/
3131
on(eventType: "data", cb: (data: any) => void): void
3232

33+
/** Does the caller want to handle input? */
34+
onInput?(data: string): void
35+
36+
/** Does the caller want to handle resizes? */
37+
onResize?(rows: number, cols: number): void
38+
3339
/**
3440
* Terminate any streaming. Will be invoked un unmount, whenever
3541
* `this.props.streamer` is given.
3642
*/
3743
unwatch(): void
3844
}
3945

40-
interface Props {
46+
type ClassName = {
47+
/** Optional class for the top-level component */
48+
className?: string
49+
}
50+
51+
interface Props extends ClassName {
52+
/** Optional font size for terminal */
53+
fontSize?: number
54+
4155
/** If given, the initial terminal output to render */
4256
initialContent?: string
4357

@@ -47,7 +61,7 @@ interface Props {
4761
watch?: WatchInit
4862
}
4963

50-
interface State {
64+
interface State extends ClassName {
5165
/** Ouch, something bad happened during the render */
5266
catastrophicError?: Error
5367

@@ -74,7 +88,10 @@ export default class XTerm extends React.PureComponent<Props, State> {
7488

7589
public constructor(props: Props) {
7690
super(props)
77-
this.state = {}
91+
this.state = {
92+
className:
93+
"flex-layout flex-column flex-align-stretch flex-fill" + (props.className ? ` ${props.className}` : ""),
94+
}
7895
}
7996

8097
public static getDerivedStateFromError(error: Error) {
@@ -91,6 +108,18 @@ export default class XTerm extends React.PureComponent<Props, State> {
91108
if (this.props.watch) {
92109
const streamer = this.props.watch()
93110
streamer.on("data", this.terminal.write.bind(this.terminal))
111+
112+
if (streamer.onInput) {
113+
// pass user input from the terminal to the watcher
114+
this.terminal.onData(streamer.onInput)
115+
this.terminal.focus()
116+
}
117+
118+
if (streamer.onResize) {
119+
const onResize = streamer.onResize
120+
this.terminal.onResize(({ rows, cols }) => onResize(rows, cols))
121+
}
122+
94123
this.setState({ streamer })
95124
}
96125
}
@@ -221,13 +250,17 @@ export default class XTerm extends React.PureComponent<Props, State> {
221250
const standIn = document.querySelector("body .repl .repl-input input")
222251
if (standIn) {
223252
const fontTheme = getComputedStyle(standIn)
224-
xterm.setOption("fontSize", parseInt(fontTheme.fontSize.replace(/px$/, ""), 10))
253+
xterm.setOption("fontSize", this.props.fontSize || parseInt(fontTheme.fontSize.replace(/px$/, ""), 10))
225254
// terminal.setOption('lineHeight', )//parseInt(fontTheme.lineHeight.replace(/px$/, ''), 10))
226255

227256
// FIXME. not tied to theme
228257
xterm.setOption("fontWeight", 400)
229258
xterm.setOption("fontWeightBold", 600)
230259
}
260+
261+
if (this.props.fontSize) {
262+
xterm.setOption("fontSize", this.props.fontSize)
263+
}
231264
} catch (err) {
232265
console.error("Error setting terminal font size", err)
233266
}
@@ -318,7 +351,7 @@ export default class XTerm extends React.PureComponent<Props, State> {
318351
return "InternalError"
319352
} else {
320353
return (
321-
<div className="flex-layout flex-column flex-align-stretch flex-fill">
354+
<div className={this.state.className}>
322355
<div ref={this.container} className="xterm-container" onKeyUp={this.onKeyUp} />
323356
{this.toolbar()}
324357
</div>

plugins/plugin-codeflare/src/controller/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ export default function registerCodeflareCommands(registrar: Registrar) {
7878
*/
7979
registrar.listen("/codeflare/dashboard-gallery", (args) => import("./hello").then((_) => _.dashboardGallery(args)))
8080

81+
/** Open a terminal */
82+
registrar.listen("/codeflare/terminal", (args) => import("./terminal").then((_) => _.default(args)), {
83+
needsUI: true,
84+
})
85+
8186
/**
8287
* Register a catch-all command handler: any `/^codeflare/` command
8388
* lines, send to madwizard.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 { Arguments, Job, ReactResponse, encodeComponent, isResizable } from "@kui-shell/core"
18+
19+
import React from "react"
20+
import { PassThrough } from "stream"
21+
22+
import respawn from "./respawn"
23+
import Terminal from "../components/Terminal"
24+
25+
/**
26+
* This is our impl of the `watch` property that our Terminal
27+
* component needs, in order to support live updates.
28+
*/
29+
function watch(stream: PassThrough, job: Job) {
30+
return {
31+
on: stream.on.bind(stream), // data from pty to terminal
32+
onInput: job.write.bind(job), // user input from terminal to pty
33+
unwatch: job.abort.bind(job), // unmount, abort pty job
34+
onResize: isResizable(job) ? job.resize.bind(job) : undefined,
35+
}
36+
}
37+
38+
/**
39+
* This is a command handler that opens up a terminal. The expectation
40+
* is that the command line to be executed is the "rest" after:
41+
* `codeflare terminal <rest...>`.
42+
*/
43+
export default function openTerminal(args: Arguments) {
44+
// eslint-disable-next-line no-async-promise-executor
45+
return new Promise<ReactResponse>(async (resolve, reject) => {
46+
try {
47+
// we need this to wire the pty output through to the Terminal
48+
// component, which expects something stream-like
49+
const passthrough = new PassThrough()
50+
51+
const { argv, env } = respawn(args.command.replace(/^\s*codeflare\s+terminal\s+/, ""))
52+
53+
await args.REPL.qexec(argv.map((_) => encodeComponent(_)).join(" "), undefined, undefined, {
54+
tab: args.tab,
55+
env,
56+
quiet: true,
57+
onInit: () => (_) => {
58+
// hooks pty output to our passthrough stream
59+
passthrough.write(_)
60+
},
61+
onReady: (job) => {
62+
resolve({
63+
react: (
64+
<div
65+
className="kui--inverted-color-context flex-fill flex-layout flex-align-stretch"
66+
style={{ backgroundColor: "var(--color-sidecar-background-02)" }}
67+
>
68+
<Terminal watch={() => watch(passthrough, job)} />
69+
</div>
70+
),
71+
})
72+
},
73+
})
74+
} catch (err) {
75+
reject(err)
76+
}
77+
})
78+
}

0 commit comments

Comments
 (0)