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

Commit dc22591

Browse files
myan9starpit
authored andcommitted
feat: add button to WatchPane to show table back in terminal
Fixes #4530
1 parent a94b8c2 commit dc22591

File tree

18 files changed

+218
-102
lines changed

18 files changed

+218
-102
lines changed

packages/core/src/models/execOptions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ export interface ExecOptions {
3636
/** true, if you wish a qexec to return rendered HTML; default is false, meaning you get the model back on qexec */
3737
render?: boolean
3838

39+
/*
40+
* always show the view in terminal, e.g. for certain commands producing watchable table,
41+
* we want them to always show in `Terminal`
42+
*/
43+
alwaysViewIn?: 'Terminal'
44+
3945
isProxied?: boolean
4046
noDelegation?: boolean
4147
delegationOk?: boolean

packages/core/src/repl/exec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,8 @@ class InProcessExecutor implements Executor {
398398
response: response || true,
399399
execUUID,
400400
echo: execOptions.echo,
401-
evaluatorOptions
401+
evaluatorOptions,
402+
execOptions
402403
}
403404
eventChannelUnsafe.emit(`/command/complete`, endEvent)
404405
eventChannelUnsafe.emit(`/command/complete/${getTabId(tab)}`, endEvent)
@@ -431,7 +432,9 @@ class InProcessExecutor implements Executor {
431432
argvNoOptions,
432433
parsedOptions,
433434
responseType,
434-
evaluatorOptions
435+
evaluatorOptions,
436+
execOptions,
437+
commandUntrimmed
435438
)
436439
})
437440
}

packages/test/src/api/selectors.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,18 @@ export const LIST_RESULT_BY_N_AND_NAME = (N: number, name: string) =>
115115
export const OK_N = (N: number) => `${PROMPT_BLOCK_N(N)} .repl-output .ok`
116116
export const xtermRows = (N: number) => `${PROMPT_BLOCK_N(N)} .xterm-container .xterm-rows`
117117

118-
export const WATCHER_N = (N: number) => `.kui--sub-pane[data-pane-index="${N}"]`
119-
export const WATCHER_N_TITLE = (N: number) => `${WATCHER_N(N)} ${_TABLE_TITLE}`
118+
export const WATCHER_N = (N: number) => `.kui--card.kui--card-${N}`
119+
120120
export const WATCHER_N_GRID_CELL = (N: number, name: string) =>
121-
`${WATCHER_N(N)} ${_TABLE_AS_GRID} [data-tag="badge"][data-entity-name="${name}"]`
121+
`${WATCHER_N(N)} .kui--sub-card ${_TABLE_AS_GRID} [data-tag="badge"][data-entity-name="${name}"]`
122122
export const WATCHER_N_GRID_CELL_ONLINE = (N: number, name: string) =>
123123
`${WATCHER_N_GRID_CELL(N, name)} .green-background`
124124
export const WATCHER_N_GRID_CELL_OFFLINE = (N: number, name: string) =>
125125
`${WATCHER_N_GRID_CELL(N, name)} .red-background`
126126
export const WATCHER_N_GRID_CELL_PENDING = (N: number, name: string) =>
127127
`${WATCHER_N_GRID_CELL(N, name)} .yello-background`
128+
129+
export const WATCHER_N_TITLE = (N: number) => `${WATCHER_N(N)} ${_TABLE_TITLE}`
130+
export const WATCHER_N_DROPDOWN = (N: number) => `${WATCHER_N(N)} .pf-c-dropdown button.pf-c-dropdown__toggle`
131+
export const WATCHER_N_DROPDOWN_ITEM = (N: number, label: string) =>
132+
`${WATCHER_N(N)} .pf-c-dropdown button.pf-c-dropdown__menu-item[data-mode="${label}"]`
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"ok": "ok"
2+
"ok": "ok",
3+
"Show as table": "Show as table"
34
}

plugins/plugin-client-common/src/components/Content/Table/PaginatedTable.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ export type State = ToolbarProps & {
7575
pageSize: number
7676
}
7777

78+
export function getBreadcrumbsFromTable(response: KuiTable, prefixBreadcrumbs: BreadcrumbView[]) {
79+
const titleBreadcrumb: BreadcrumbView[] = response.title
80+
? [{ label: response.title, className: 'kui--data-table-title' }]
81+
: []
82+
83+
const breadcrumbs = (prefixBreadcrumbs || [])
84+
.concat(titleBreadcrumb)
85+
.concat((response.breadcrumbs || []).map(_ => Object.assign({}, _, { className: 'kui--secondary-breadcrumb' })))
86+
87+
return breadcrumbs
88+
}
89+
7890
/**
7991
* A DataTable/Pagination pair
8092
*
@@ -104,17 +116,7 @@ export default class PaginatedTable<P extends Props, S extends State> extends Re
104116

105117
private topToolbar() {
106118
if (this.props.toolbars) {
107-
const titleBreadcrumb: BreadcrumbView[] = this.props.response.title
108-
? [{ label: this.props.response.title, className: 'kui--data-table-title' }]
109-
: []
110-
const breadcrumbs = (this.props.prefixBreadcrumbs || [])
111-
.concat(titleBreadcrumb)
112-
.concat(
113-
(this.props.response.breadcrumbs || []).map(_ =>
114-
Object.assign({}, _, { className: 'kui--secondary-breadcrumb' })
115-
)
116-
)
117-
119+
const breadcrumbs = getBreadcrumbsFromTable(this.props.response, this.props.prefixBreadcrumbs)
118120
return <Toolbar className="kui--data-table-toolbar-top" breadcrumbs={breadcrumbs.length > 0 && breadcrumbs} />
119121
}
120122
}

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as React from 'react'
1818
import { Accordion } from 'carbon-components-react'
1919
import {
2020
eventChannelUnsafe,
21+
ExecOptions,
2122
ScalarResponse,
2223
Tab as KuiTab,
2324
isPopup,
@@ -116,8 +117,15 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
116117
response,
117118
cancelled,
118119
echo,
119-
evaluatorOptions
120-
}: { response: ScalarResponse; cancelled: boolean; echo: boolean; evaluatorOptions: CommandOptions },
120+
evaluatorOptions,
121+
execOptions
122+
}: {
123+
response: ScalarResponse
124+
cancelled: boolean
125+
echo: boolean
126+
evaluatorOptions: CommandOptions
127+
execOptions: ExecOptions
128+
},
121129
execUUID: string,
122130
responseType: string
123131
) {
@@ -131,7 +139,9 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
131139

132140
// response `showInTerminal` is either non-watchable response, or watch response that's forced to show in terminal
133141
const showInTerminal =
134-
!isWatchable(response) || (isWatchable(response) && evaluatorOptions.alwaysViewIn === 'Terminal')
142+
!isWatchable(response) ||
143+
(isWatchable(response) &&
144+
(evaluatorOptions.alwaysViewIn === 'Terminal' || execOptions.alwaysViewIn === 'Terminal'))
135145

136146
if (inProcessIdx >= 0) {
137147
const inProcess = curState.blocks[inProcessIdx]

plugins/plugin-client-common/src/components/Views/WatchPane.tsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { v4 as uuid } from 'uuid'
1919

2020
import {
2121
eventChannelUnsafe,
22+
ExecOptions,
23+
i18n,
2224
isWatchable,
2325
ScalarResponse,
2426
Tab,
@@ -33,7 +35,11 @@ import CardSpi from '../spi/Card'
3335
import sameCommand from './util/same'
3436
import { cwd as getCwd } from './Sidecar/BaseSidecar'
3537
import LivePaginatedTable from '../Content/Table/LivePaginatedTable'
38+
import { getBreadcrumbsFromTable } from '../Content/Table/PaginatedTable'
3639
import CircularBuffer, { BaseHistoryEntry } from './util/CircularBuffer'
40+
import Breadcrumb from '../spi/Breadcrumb'
41+
42+
const strings = i18n('plugin-client-common')
3743

3844
/*
3945
* Height defines the primary height of
@@ -52,6 +58,7 @@ interface Props {
5258
}
5359

5460
interface HistoryEntry extends BaseHistoryEntry {
61+
command: string
5562
key: string // helps react distinguish similar Table
5663
response: Table & Watchable
5764
}
@@ -82,9 +89,16 @@ export default class WatchPane extends React.PureComponent<Props, State> {
8289
argvNoOptions: string[],
8390
parsedOptions: ParsedOptions,
8491
__,
85-
evaluatorOptions: CommandOptions
92+
evaluatorOptions: CommandOptions,
93+
execOptions: ExecOptions,
94+
command: string
8695
) {
87-
if (isTable(response) && isWatchable(response) && evaluatorOptions.alwaysViewIn !== 'Terminal') {
96+
if (
97+
isTable(response) &&
98+
isWatchable(response) &&
99+
evaluatorOptions.alwaysViewIn !== 'Terminal' &&
100+
execOptions.alwaysViewIn !== 'Terminal'
101+
) {
88102
this.setState(curState => {
89103
const cwd = getCwd()
90104

@@ -97,6 +111,7 @@ export default class WatchPane extends React.PureComponent<Props, State> {
97111
response,
98112
argvNoOptions,
99113
parsedOptions,
114+
command,
100115
cwd
101116
}
102117

@@ -130,8 +145,22 @@ export default class WatchPane extends React.PureComponent<Props, State> {
130145
return 4
131146
}
132147

133-
private prefixBreadcrumbs(idx: number) {
134-
return [{ label: `Watcher ${idx + 1}` }]
148+
/** re-execute the command, but display the watch result in terminal */
149+
private watchInTerminal(command: string) {
150+
this.props.tab.REPL.pexec(command, { alwaysViewIn: 'Terminal' })
151+
}
152+
153+
/** `Card Actions`, will be rendred as `Dropdown` */
154+
private actions(command: string) {
155+
const watchInTerminal = { label: strings('Show as table'), handler: this.watchInTerminal.bind(this, command) }
156+
return [watchInTerminal]
157+
}
158+
159+
/** render subpane header as Breadcrumb */
160+
private header(response: Table, idx: number) {
161+
const prefixBreadcrumbs = [{ label: `Watcher ${idx + 1}` }]
162+
const breadcrumbs = getBreadcrumbsFromTable(response, prefixBreadcrumbs)
163+
return <Breadcrumb repl={this.props.tab.REPL} breadcrumbs={breadcrumbs.length > 0 && breadcrumbs} />
135164
}
136165

137166
public render() {
@@ -143,17 +172,21 @@ export default class WatchPane extends React.PureComponent<Props, State> {
143172
.map((_, idx) => {
144173
const history = this.state.history.peekAt(idx)
145174
return (
146-
<CardSpi className="kui--card kui--screenshotable" key={history ? history.key : idx}>
175+
<CardSpi
176+
className={`kui--card kui--screenshotable kui--card-${idx + 1}`}
177+
actions={history && this.actions(history.command)}
178+
header={history && this.header(history.response, idx)}
179+
key={history ? history.key : idx}
180+
>
147181
{history ? (
148-
<div className="kui--sub-pane" data-pane-index={idx + 1}>
182+
<div className="kui--sub-card">
149183
<LivePaginatedTable
150184
tab={this.props.tab}
151185
repl={this.props.tab.REPL}
152186
response={history.response}
153187
asGrid
154-
toolbars
188+
toolbars={false}
155189
paginate={false}
156-
prefixBreadcrumbs={this.prefixBreadcrumbs(idx)}
157190
/>
158191
</div>
159192
) : (

plugins/plugin-client-common/src/components/spi/Card/impl/Carbon.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

plugins/plugin-client-common/src/components/spi/Card/impl/PatternFly.tsx

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,70 @@
1515
*/
1616

1717
import * as React from 'react'
18-
import { Card, CardBody } from '@patternfly/react-core'
18+
import Props, { Action } from '../model'
1919

20-
import Props from '../model'
20+
import {
21+
Card,
22+
CardActions,
23+
CardBody,
24+
CardHead,
25+
CardHeadMain,
26+
Dropdown,
27+
DropdownItem,
28+
KebabToggle
29+
} from '@patternfly/react-core'
2130

2231
import '../../../../../web/scss/components/Card/Patternfly.scss'
2332

24-
export default class PatternflyCard extends React.PureComponent<Props> {
33+
interface State {
34+
isOpen: boolean
35+
}
36+
37+
export default class PatternflyCard extends React.PureComponent<Props, State> {
38+
public constructor(props: Props) {
39+
super(props)
40+
41+
this.state = {
42+
isOpen: false
43+
}
44+
}
45+
46+
private renderDropDownItems(actions: Action[]) {
47+
return actions.map(item => (
48+
<DropdownItem key="action" component="button" onClick={item.handler} data-mode={item.label}>
49+
{item.label}
50+
</DropdownItem>
51+
))
52+
}
53+
54+
private cardActions() {
55+
return (
56+
<CardActions>
57+
<Dropdown
58+
onSelect={() => this.setState({ isOpen: !this.state.isOpen })}
59+
toggle={
60+
<KebabToggle
61+
onToggle={isOpen => {
62+
this.setState({ isOpen })
63+
}}
64+
/>
65+
}
66+
isOpen={this.state.isOpen}
67+
isPlain
68+
dropdownItems={this.renderDropDownItems(this.props.actions)}
69+
position={'right'}
70+
/>
71+
</CardActions>
72+
)
73+
}
74+
2575
public render() {
2676
return (
2777
<Card className={this.props.className}>
78+
<CardHead>
79+
<CardHeadMain>{this.props.header}</CardHeadMain>
80+
{this.props.actions && this.cardActions()}
81+
</CardHead>
2882
<CardBody>{this.props.children}</CardBody>
2983
</Card>
3084
)

plugins/plugin-client-common/src/components/spi/Card/index.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,11 @@
1616

1717
import * as React from 'react'
1818

19-
import Carbon from './impl/Carbon'
2019
import PatternFly4 from './impl/PatternFly'
21-
import KuiContext from '../../Client/context'
2220

2321
import Props from './model'
2422

23+
// FIXME There's no ideal Card component in Carbon Component Libary, so we use Patternfly
2524
export default function CardSpi(props: Props): React.ReactElement {
26-
return (
27-
<KuiContext.Consumer>
28-
{config => (config.components === 'patternfly' ? <PatternFly4 {...props} /> : <Carbon {...props} />)}
29-
</KuiContext.Consumer>
30-
)
25+
return <PatternFly4 {...props} />
3126
}

0 commit comments

Comments
 (0)