Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit 5744bf4

Browse files
committed
feat: initial support for capturing input files and showing them in the UI
Fixes #5513
1 parent 893902e commit 5744bf4

File tree

24 files changed

+656
-33
lines changed

24 files changed

+656
-33
lines changed

packages/core/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export {
5454
MixedResponse,
5555
isMixedResponse,
5656
RawResponse,
57-
// ResourceModification,
57+
hasSourceReferences,
58+
WithSourceReferences,
59+
SourceRef,
5860
MetadataBearingByReference as ResourceByReference,
5961
MetadataBearingByReferenceWithContent as ResourceByReferenceWithContent,
6062
isMetadataBearingByReference as isResourceByReference

packages/core/src/models/entity.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export function isRawResponse<Content extends RawContent>(entity: Entity<Content
185185
*/
186186
export type ScalarResponse<RowType extends Row = Row> =
187187
| SimpleEntity
188-
| Table<RowType>
188+
| (Table<RowType> & Partial<WithSourceReferences>)
189189
| MixedResponse
190190
| CommentaryResponse
191191

@@ -200,6 +200,22 @@ export type StructuredResponse<
200200
SomeSortOfResource extends MetadataBearing<Content> = MetadataBearing<Content>
201201
> = ViewableResponse | UsageModel | SomeSortOfResource | RawResponse<Content> | SomeSortOfResource[]
202202

203+
export interface SourceRef {
204+
templates: { filepath: string; data: string; isFor: string; kind?: 'source' | 'template'; contentType?: string }[]
205+
customization?: { filepath: string; data: string; isFor: string }
206+
}
207+
208+
export interface WithSourceReferences {
209+
kuiSourceRef: SourceRef
210+
}
211+
212+
export function hasSourceReferences(
213+
response: Partial<WithSourceReferences>
214+
): response is Required<WithSourceReferences> {
215+
const trait = response as WithSourceReferences
216+
return trait && trait.kuiSourceRef !== undefined
217+
}
218+
203219
/**
204220
* A potentially more complex entity with a "spec"
205221
*

packages/test/src/api/selectors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,9 @@ export const RADIO_BUTTON = '.kui--radio-table-body .kui--radio-table-row'
239239
export const RADIO_BUTTON_BY_NAME = (name: string) => `${RADIO_BUTTON}[data-name="${name}"]`
240240
export const RADIO_BUTTON_IS_SELECTED = '[data-is-selected]'
241241
export const RADIO_BUTTON_SELECTED = `${RADIO_BUTTON}${RADIO_BUTTON_IS_SELECTED}`
242+
243+
/** SourceRef */
244+
export const SOURCE_REF_N = (N: number, splitIndex = 1) =>
245+
`${PROMPT_BLOCK_N_FOR_SPLIT(N, splitIndex)} .kui--expandable-section`
246+
export const SOURCE_REF_TOGGLE_N = (N: number, expanded = false, splitIndex = 1) =>
247+
`${SOURCE_REF_N(N, splitIndex)} .pf-c-expandable-section__toggle[aria-expanded=${expanded.toString()}]`

packages/test/src/api/util.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ export const expectArray = (expected: Array<string>, failFast = true, subset = f
166166
}
167167

168168
/** get the monaco editor text */
169-
export const getValueFromMonaco = async (app: Application) => {
170-
const selector = '.bx--tab-content[aria-hidden="false"] .monaco-editor-wrapper'
169+
export const getValueFromMonaco = async (app: Application, container = '.bx--tab-content[aria-hidden="false"]') => {
170+
const selector = `${container} .monaco-editor-wrapper`
171171
try {
172172
await app.client.waitForExist(selector, CLI.waitTimeout)
173173
} catch (err) {

plugins/plugin-client-common/i18n/resources_en_US.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
"No resources": "No resources",
88
"nRows": "{0} rows",
99
"nRows1": "1 row",
10+
"Show More": "Show More",
11+
"Show Less": "Show Less",
12+
"Show X": "Show {0}",
13+
"Hide X": "Hide {0}",
1014
"New Tab": "Open a new tab",
1115
"Duration": "Duration: {0}",
1216
"Cold Start": "Cold start delay: {0}. {1}",
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2020 IBM Corporation
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 * as React from 'react'
18+
import { editor as Monaco } from 'monaco-editor'
19+
20+
import { eventChannelUnsafe, eventBus } from '@kui-shell/core'
21+
22+
import getKuiFontSize from './lib/fonts'
23+
import defaultMonacoOptions, { Options as MonacoOptions } from './lib/defaults'
24+
25+
import '../../../../web/scss/components/Editor/Editor.scss'
26+
27+
type Props = Pick<MonacoOptions, 'fontSize'> & {
28+
tabUUID: string
29+
content: string
30+
contentType: string
31+
className?: string
32+
}
33+
34+
interface State {
35+
editor: Monaco.ICodeEditor
36+
wrapper: HTMLDivElement
37+
catastrophicError: Error
38+
cleaners: (() => void)[]
39+
}
40+
41+
export default class SimpleEditor extends React.PureComponent<Props, State> {
42+
public constructor(props: Props) {
43+
super(props)
44+
45+
// created below in render() via ref={...} -> initMonaco()
46+
this.state = {
47+
cleaners: [],
48+
editor: undefined,
49+
wrapper: undefined,
50+
catastrophicError: undefined
51+
}
52+
}
53+
54+
public static getDerivedStateFromError(error: Error) {
55+
return { catastrophicError: error }
56+
}
57+
58+
public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
59+
console.error('catastrophic error in Editor', error, errorInfo)
60+
}
61+
62+
/** Called whenever we have proposed (props,state); we derive a new State */
63+
public static getDerivedStateFromProps(props: Props, state: State) {
64+
if (!state.editor && state.wrapper) {
65+
// then we are ready to render monaco into the wrapper
66+
return SimpleEditor.initMonaco(props, state)
67+
} else {
68+
return state
69+
}
70+
}
71+
72+
/** Called when this component is no longer attached to the document */
73+
public componentWillUnmount() {
74+
this.destroyMonaco()
75+
}
76+
77+
/** Called when we no longer need the monaco-editor instance */
78+
private destroyMonaco() {
79+
this.state.cleaners.forEach(cleaner => cleaner())
80+
}
81+
82+
/** Called when we have a ready wrapper (monaco's init requires an wrapper */
83+
private static initMonaco(props: Props, state: State): Partial<State> {
84+
const cleaners = []
85+
86+
try {
87+
// here we instantiate an editor widget
88+
const providedOptions = {
89+
value: props.content,
90+
readOnly: true,
91+
fontSize: props.fontSize,
92+
language: props.contentType
93+
}
94+
const options = Object.assign(defaultMonacoOptions(providedOptions), providedOptions)
95+
const editor = Monaco.create(state.wrapper, options)
96+
97+
state.wrapper['getValueForTests'] = () => {
98+
return editor.getValue()
99+
}
100+
101+
const onZoom = () => {
102+
editor.updateOptions({ fontSize: getKuiFontSize() })
103+
}
104+
eventChannelUnsafe.on('/zoom', onZoom)
105+
cleaners.push(() => eventChannelUnsafe.off('/zoom', onZoom))
106+
107+
const onTabLayoutChange = () => {
108+
editor.layout()
109+
}
110+
eventBus.onTabLayoutChange(props.tabUUID, onTabLayoutChange)
111+
cleaners.push(() => eventBus.offTabLayoutChange(props.tabUUID, onTabLayoutChange))
112+
113+
cleaners.push(() => {
114+
editor.dispose()
115+
const model = editor.getModel()
116+
if (model) {
117+
model.dispose()
118+
}
119+
})
120+
121+
return {
122+
editor,
123+
cleaners
124+
}
125+
} catch (err) {
126+
console.error('Error initing Monaco: ', err)
127+
state.catastrophicError = err
128+
return state
129+
}
130+
}
131+
132+
public render() {
133+
if (this.state.catastrophicError) {
134+
return <div className="oops"> {this.state.catastrophicError.toString()}</div>
135+
} else {
136+
const className = 'monaco-editor-wrapper' + (this.props.className ? ' ' + this.props.className : '')
137+
return <div className={className} ref={wrapper => this.setState({ wrapper })}></div>
138+
}
139+
}
140+
}

plugins/plugin-client-common/src/components/Content/Editor/index.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727
ToolbarText,
2828
ToolbarProps,
2929
MultiModalResponse,
30-
TabLayoutChangeEvent,
3130
eventChannelUnsafe,
3231
eventBus,
3332
i18n
@@ -294,10 +293,8 @@ export default class Editor extends React.PureComponent<Props, State> {
294293
eventChannelUnsafe.on('/zoom', onZoom)
295294
cleaners.push(() => eventChannelUnsafe.off('/zoom', onZoom))
296295

297-
const onTabLayoutChange = (evt: TabLayoutChangeEvent) => {
298-
if (!evt.isSidecarNowHidden) {
299-
editor.layout()
300-
}
296+
const onTabLayoutChange = () => {
297+
editor.layout()
301298
}
302299
eventBus.onTabLayoutChange(props.tabUUID, onTabLayoutChange)
303300
cleaners.push(() => eventBus.offTabLayoutChange(props.tabUUID, onTabLayoutChange))

plugins/plugin-client-common/src/components/Content/Editor/lib/defaults.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import getKuiFontSize from './fonts'
2020
export interface Options {
2121
readOnly?: boolean
2222
simple?: boolean
23+
fontSize?: number
2324
}
2425

2526
export default (options: Options): editor.IEditorConstructionOptions => ({
@@ -35,7 +36,7 @@ export default (options: Options): editor.IEditorConstructionOptions => ({
3536
scrollBeyondLastColumn: 2,
3637
// cursorStyle: 'block',
3738
fontFamily: 'var(--font-monospace)',
38-
fontSize: getKuiFontSize(),
39+
fontSize: options.fontSize || getKuiFontSize(),
3940

4041
// specifics for readOnly mode
4142
glyphMargin: !options.readOnly, // needed for error indicators

plugins/plugin-client-common/src/components/Views/Terminal/Block/Input.tsx

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,34 @@
1515
*/
1616

1717
import * as React from 'react'
18+
import { basename } from 'path'
1819
import { dots as spinnerFrames } from 'cli-spinners'
19-
import { Tab as KuiTab, inBrowser, doCancel, i18n } from '@kui-shell/core'
20+
import { Tab as KuiTab, inBrowser, doCancel, i18n, isTable, hasSourceReferences, eventBus } from '@kui-shell/core'
2021

2122
import onPaste from './OnPaste'
2223
import onKeyDown from './OnKeyDown'
2324
import onKeyPress from './OnKeyPress'
2425
import KuiContext from '../../../Client/context'
2526
import { TabCompletionState } from './TabCompletion'
2627
import ActiveISearch, { onKeyUp } from './ActiveISearch'
27-
import { BlockModel, isActive, isProcessing, isFinished, hasCommand, isEmpty, hasUUID, hasValue } from './BlockModel'
28+
import {
29+
BlockModel,
30+
isActive,
31+
isProcessing,
32+
isFinished,
33+
hasCommand,
34+
isEmpty,
35+
isWithCompleteEvent,
36+
hasUUID,
37+
hasValue
38+
} from './BlockModel'
2839
import { BlockViewTraits } from './'
2940

30-
import DropDown, { DropDownAction } from '../../../spi/DropDown'
3141
import Tag from '../../../spi/Tag'
42+
import ExpandableSection from '../../../spi/ExpandableSection'
43+
import DropDown, { DropDownAction } from '../../../spi/DropDown'
44+
45+
const SimpleEditor = React.lazy(() => import('../../../Content/Editor/SimpleEditor'))
3246

3347
const strings = i18n('plugin-client-common')
3448
const strings2 = i18n('plugin-client-common', 'screenshot')
@@ -198,17 +212,59 @@ export abstract class InputProvider<S extends State = State> extends React.PureC
198212
}
199213
}
200214

215+
protected sourceRef() {
216+
const { model } = this.props
217+
218+
if (model && isWithCompleteEvent(model) && isTable(model.response) && hasSourceReferences(model.response)) {
219+
const sourceRef = model.response.kuiSourceRef
220+
return (
221+
<div className="repl-input-sourceref">
222+
<div className="repl-context"></div>
223+
<div className="flex-layout flex-fill">
224+
{sourceRef.templates.map((_, idx) => {
225+
const name = basename(_.filepath)
226+
return (
227+
<ExpandableSection
228+
key={idx}
229+
className="flex-fill"
230+
showMore={strings('Show X', name)}
231+
showLess={strings('Hide X', name)}
232+
onToggle={() => eventBus.emitTabLayoutChange(this.props.tab.uuid)}
233+
>
234+
<SimpleEditor
235+
tabUUID={this.props.tab.uuid}
236+
content={_.data}
237+
contentType={_.contentType}
238+
className="kui--source-ref-editor kui--inverted-color-context"
239+
fontSize={12}
240+
/>
241+
</ExpandableSection>
242+
)
243+
})}
244+
</div>
245+
</div>
246+
)
247+
}
248+
249+
// if (this.state.sourceRef) {
250+
// return 'hi'
251+
// }
252+
}
253+
201254
public render() {
202255
return (
203-
<div className={'repl-input' + (this.state && this.state.isearch ? ' kui--isearch-active' : '')}>
204-
{this.prompt()}
205-
<div className="kui--input-and-context">
206-
{this.props.children}
207-
{this.input()}
208-
{this.status()}
256+
<React.Fragment>
257+
<div className={'repl-input' + (this.state && this.state.isearch ? ' kui--isearch-active' : '')}>
258+
{this.prompt()}
259+
<div className="kui--input-and-context">
260+
{this.props.children}
261+
{this.input()}
262+
{this.status()}
263+
</div>
264+
{this.state && this.state.tabCompletion && this.state.tabCompletion.render()}
209265
</div>
210-
{this.state && this.state.tabCompletion && this.state.tabCompletion.render()}
211-
</div>
266+
{this.sourceRef()}
267+
</React.Fragment>
212268
)
213269
}
214270
}

0 commit comments

Comments
 (0)