Skip to content

Commit

Permalink
feat: Meta-click on table entries should open the drilldown in a diff…
Browse files Browse the repository at this point in the history
…erent split

Fixes #6403
  • Loading branch information
starpit committed Dec 15, 2020
1 parent 0eca8c3 commit 5d3f032
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 188 deletions.
364 changes: 204 additions & 160 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"@types/react-dom": "16.9.8",
"@types/swagger-schema-official": "2.0.21",
"@types/tmp": "0.2.0",
"@types/turndown": "^5.0.0",
"@types/turndown": "5.0.0",
"@types/uuid": "8.3.0",
"@types/which": "1.3.2",
"@types/yargs-parser": "15.0.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/models/TabLayoutModificationResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export type NewSplitRequest = {

/** Use an inverted color scheme for the new split? */
inverseColors?: boolean

/** Execute this command line in the new split */
cmdline?: string

/** Only perform the split if the given command returns true */
if?: string

/** Only perform the split if the given command returns false */
ifnot?: string
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/webapp/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface Tab extends HTMLDivElement {
scrollToTop(): void
scrollToBottom(): void
getSize(): { width: number; height: number }

splitCount(): number
}

export function isTab(node: Element | Tab): node is Tab {
Expand Down Expand Up @@ -104,7 +106,7 @@ export function pexecInCurrentTab(command: string, topLevelTab?: Tab, isInternal
const { facade: tab } = scrollback
return isInternalCallpath
? tab.REPL.qexec(command, undefined, undefined, { tab })
: tab.REPL.pexec(command, { tab, echo: !incognito })
: tab.REPL.pexec(command, { tab, echo: !incognito, noHistory: true })
} else {
return Promise.reject(
new Error(
Expand Down
40 changes: 39 additions & 1 deletion packages/test/src/api/keys.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/*
* Copyright 2019-2020 IBM Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ISuite } from './common'

export const keys = {
Numpad1: '\uE01B',
Numpad2: '\uE01C',
Expand All @@ -18,5 +36,25 @@ export const keys = {
// Send NULL to release Control key at the end of the call, otherwise the state of Control is kept between calls
ctrlN: ['\uE009', 'n', 'NULL'],
ctrlP: ['\uE009', 'p', 'NULL'],
ctrlC: ['\uE009', 'c', 'NULL']
ctrlC: ['\uE009', 'c', 'NULL'],

holdDownKey: function(this: ISuite, character: string) {
return this.app.client.performActions([
{
type: 'key',
id: 'keyboard',
actions: [{ type: 'keyDown', value: character }]
}
])
},

releaseKey: function(this: ISuite, character: string) {
return this.app.client.performActions([
{
type: 'key',
id: 'keyboard',
actions: [{ type: 'keyUp', value: character }]
}
])
}
}
23 changes: 14 additions & 9 deletions packages/test/src/api/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,24 @@ export const STATUS_STRIPE_BLOCK = '.kui--status-stripe .kui--input-stripe .repl
export const STATUS_STRIPE_PROMPT = `${STATUS_STRIPE_BLOCK} input`
export const OOPS = `${CURRENT_TAB} .repl .repl-block .oops`
export const _SIDECAR = '.kui--sidecar'
export const SIDECAR_BASE = (N: number) => `${PROMPT_BLOCK_N(N)} ${_SIDECAR}`
export const SIDECAR = (N: number) => `${SIDECAR_BASE(N)}.visible:not(.minimized)`
export const SIDECAR_FULLSCREEN = (N: number) => `${SIDECAR(N)}.maximized`
export const SIDECAR_WITH_FAILURE = (N: number) => `${SIDECAR_BASE(N)}.visible.activation-success-false`
export const SIDECAR_ACTIVATION_TITLE = (N: number) => `${SIDECAR(N)} .kui--sidecar-entity-name-hash .bx--link`
export const SIDECAR_TITLE = (N: number) => `${SIDECAR(N)} .kui--sidecar-entity-name .bx--link`
// eslint-disable-next-line @typescript-eslint/no-use-before-define
export const SIDECAR_BASE = (N: number, splitIndex = 1) => `${PROMPT_BLOCK_N_FOR_SPLIT(N, splitIndex)} ${_SIDECAR}`
export const SIDECAR = (N: number, splitIndex = 1) => `${SIDECAR_BASE(N, splitIndex)}.visible:not(.minimized)`
export const SIDECAR_FULLSCREEN = (N: number, splitIndex = 1) => `${SIDECAR(N, splitIndex)}.maximized`
export const SIDECAR_WITH_FAILURE = (N: number, splitIndex = 1) =>
`${SIDECAR_BASE(N, splitIndex)}.visible.activation-success-false`
export const SIDECAR_ACTIVATION_TITLE = (N: number, splitIndex = 1) =>
`${SIDECAR(N, splitIndex)} .kui--sidecar-entity-name-hash .bx--link`
export const SIDECAR_TITLE = (N: number, splitIndex = 1, clickable = true) =>
`${SIDECAR(N, splitIndex)} .kui--sidecar-entity-name` + (clickable ? ' .bx--link' : '')
export const SIDECAR_HERO_TITLE = (N: number) => `${SIDECAR(N)} .sidecar-header .sidecar-header-name`
export const SIDECAR_LEFTNAV_TITLE = (N: number) =>
`${SIDECAR(N)} .sidecar-header-name-content .bx--side-nav__submenu-title`
export const SIDECAR_LEFTNAV_TITLE = (N: number, splitIndex = 1) =>
`${SIDECAR(N, splitIndex)} .sidecar-header-name-content .bx--side-nav__submenu-title`
export const SIDECAR_HEADER_NAVIGATION = (N: number) => `${SIDECAR(N)} .kui--sidecar--titlebar-navigation`
export const SIDECAR_BREADCRUMBS = (N: number) =>
`${SIDECAR_HEADER_NAVIGATION(N)} .bx--breadcrumb .bx--breadcrumb-item .bx--link`
export const SIDECAR_PACKAGE_NAME_TITLE = (N: number) => `${SIDECAR(N)} .kui--sidecar-entity-namespace .bx--link`
export const SIDECAR_PACKAGE_NAME_TITLE = (N: number, splitIndex = 1) =>
`${SIDECAR(N, splitIndex)} .kui--sidecar-entity-namespace .bx--link`
export const SIDECAR_POPUP_TITLE = SIDECAR_TITLE
export const SIDECAR_POPUP_HERO_TITLE = SIDECAR_HERO_TITLE
export const SIDECAR_KIND = (N: number) => `${SIDECAR(N)} .kui--sidecar-kind .bx--link`
Expand Down
25 changes: 17 additions & 8 deletions packages/test/src/api/sidecar-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import { expectArray, expectText, getValueFromMonaco, expectYAMLSubset } from '.
import { AppAndCount, blockAfter } from './repl-expect'

export const open = async (res: AppAndCount) => {
await res.app.client.$(Selectors.SIDECAR(res.count)).then(_ => _.waitForDisplayed({ timeout: waitTimeout }))
await res.app.client
.$(Selectors.SIDECAR(res.count, res.splitIndex))
.then(_ => _.waitForDisplayed({ timeout: waitTimeout }))
return res
}

Expand All @@ -35,14 +37,14 @@ export function openInBlockAfter(res: AppAndCount, delta = 1) {

export const notOpen = async (res: AppAndCount) => {
await res.app.client
.$(Selectors.SIDECAR(res.count))
.$(Selectors.SIDECAR(res.count, res.splitIndex))
.then(_ => _.waitForDisplayed({ timeout: waitTimeout, reverse: true }))
return res
}

export const openWithFailure = async (res: AppAndCount) => {
return res.app.client
.$(Selectors.SIDECAR_WITH_FAILURE(res.count))
.$(Selectors.SIDECAR_WITH_FAILURE(res.count, res.splitIndex))
.then(_ => _.waitForDisplayed({ timeout: waitTimeout }))
.then(() => res)
}
Expand Down Expand Up @@ -308,20 +310,25 @@ export const showing = (
expectedPackageName?: string,
expectType?: string,
waitThisLong?: number,
which?: 'leftnav' | 'topnav'
which?: 'leftnav' | 'topnav',
clickable?: boolean
) => async (res: AppAndCount): Promise<AppAndCount> => {
await res.app.client.waitUntil(
async () => {
// check selected name in sidecar
const sidecarSelector = `${Selectors.SIDECAR(res.count)}${!expectType ? '' : '.entity-is-' + expectType}`
const sidecarSelector = `${Selectors.SIDECAR(res.count, res.splitIndex)}${
!expectType ? '' : '.entity-is-' + expectType
}`
await res.app.client.$(sidecarSelector).then(_ => _.waitForDisplayed())

// either 'leftnav' or 'topnav'
if (!which) {
which = (await res.app.client.$(sidecarSelector).then(_ => _.getAttribute('data-view'))) as 'leftnav' | 'topnav'
}
const titleSelector =
which === 'topnav' ? Selectors.SIDECAR_TITLE(res.count) : Selectors.SIDECAR_LEFTNAV_TITLE(res.count)
which === 'topnav'
? Selectors.SIDECAR_TITLE(res.count, res.splitIndex, clickable)
: Selectors.SIDECAR_LEFTNAV_TITLE(res.count, res.splitIndex)

return res.app.client
.$(titleSelector)
Expand All @@ -333,7 +340,7 @@ export const showing = (
if (nameMatches) {
if (expectedPackageName) {
return res.app.client
.$(Selectors.SIDECAR_PACKAGE_NAME_TITLE(res.count))
.$(Selectors.SIDECAR_PACKAGE_NAME_TITLE(res.count, res.splitIndex))
.then(_ => _.getText())
.then(name =>
expectSubstringMatchOnName
Expand All @@ -356,7 +363,9 @@ export const showing = (
await res.app.client.waitUntil(
async () => {
try {
const actualId = await res.app.client.$(Selectors.SIDECAR_ACTIVATION_TITLE(res.count)).then(_ => _.getText())
const actualId = await res.app.client
.$(Selectors.SIDECAR_ACTIVATION_TITLE(res.count, res.splitIndex))
.then(_ => _.getText())
if (actualId === expectedActivationId) {
return true
} else {
Expand Down
3 changes: 2 additions & 1 deletion packages/test/src/api/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 IBM Corporation
* Copyright 2019-2020 IBM Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as assert from 'assert'
import { Application } from 'spectron'
import { v4 as uuid } from 'uuid'
Expand Down
41 changes: 40 additions & 1 deletion plugins/plugin-bash-like/src/test/bash-like/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { Common, CLI, ReplExpect } from '@kui-shell/test'
import { Common, CLI, ReplExpect, Selectors, SidecarExpect, Keys } from '@kui-shell/test'

const echoString = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

Expand Down Expand Up @@ -43,6 +43,45 @@ describe(`directory listing ${process.env.MOCHA_RUN_TARGET || ''}`, function(thi
.then(ReplExpect.okWith('package.json'))
.catch(Common.oops(this)))

const doListAndClick = async () => {
const holdDown = Keys.holdDownKey.bind(this)
const release = Keys.releaseKey.bind(this)

const res = await CLI.command(`ls -l ../../`, this.app)
await ReplExpect.okWith('package.json')

const selector = Selectors.LIST_RESULT_BY_N_FOR_NAME(res.count, 'package.json')
await holdDown(Keys.META)
await this.app.client.$(selector).then(_ => _.click())
await release(Keys.META)

return res
}
it('list and click, and drilldown should be in a new split', async () => {
try {
await ReplExpect.splitCount(1)
await doListAndClick()
await ReplExpect.splitCount(2)
await SidecarExpect.open({ app: this.app, count: 0, splitIndex: 2 }).then(
SidecarExpect.showing('package.json', undefined, undefined, undefined, undefined, undefined, undefined, false)
)
} catch (err) {
await Common.oops(this, true)(err)
}
})
it('list and click again, and drilldown should be in that same new split', async () => {
try {
await ReplExpect.splitCount(2)
await doListAndClick()
await ReplExpect.splitCount(2)
await SidecarExpect.open({ app: this.app, count: 1, splitIndex: 2 }).then(
SidecarExpect.showing('package.json', undefined, undefined, undefined, undefined, undefined, undefined, false)
)
} catch (err) {
await Common.oops(this, true)(err)
}
})

it('should ls with semicolons 1', () =>
CLI.command(`ls -l ../../ ; echo ${echoString}`, this.app)
.then(ReplExpect.okWith('package.json'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@
import React from 'react'
import prettyPrintDuration from 'pretty-ms'
import { TableCell, DataTableCell } from 'carbon-components-react'
import { Table as KuiTable, Cell as KuiCell, Row as KuiRow, Tab, REPL, eventBus } from '@kui-shell/core'
import {
Table as KuiTable,
Cell as KuiCell,
Row as KuiRow,
Tab,
REPL,
eventBus,
pexecInCurrentTab
} from '@kui-shell/core'

import Markdown from '../Markdown'
import ErrorCell from './ErrorCell'
Expand Down Expand Up @@ -59,7 +67,7 @@ export function onClickForCell(
evt.stopPropagation()
selectRow()
if (evt.metaKey) {
repl.pexec(`split --cmdline "${handler}"`)
pexecInCurrentTab(`split --ifnot is-split --cmdline "${handler}"`, undefined, false, true)
} else {
repl.pexec(handler, opts)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,12 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
eventBus.onceWithTabId('/tab/close/request', sbuuid, onTabCloseRequest)
state.cleaners.push(() => eventBus.offWithTabId('/tab/close/request', sbuuid, onTabCloseRequest))

if (opts.cmdline) {
setTimeout(() => {
state.facade.REPL.pexec(opts.cmdline)
})
}

return this.initEvents(state)
}

Expand Down Expand Up @@ -601,7 +607,9 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
if (execType === ExecType.ClickHandler) {
// <-- this is a click handler event
const idx = this.findSplit(this.state, sbuuid)
if (this.isMiniSplit(this.state.splits[idx], idx)) {
// note: idx may be < 0 if we are executing a command in-flight,
// e.g. executing a command in another split
if (idx >= 0 && this.isMiniSplit(this.state.splits[idx], idx)) {
// <-- this is a minisplit
const plainSplit = this.state.splits.find((split, idx) => !this.isMiniSplit(split, idx))
if (plainSplit) {
Expand Down Expand Up @@ -688,7 +696,7 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
}

/** the REPL finished executing a command */
public onExecEnd(
public async onExecEnd(
uuid = this.currentUUID,
asReplay: boolean,
event: CommandCompleteEvent<ScalarResponse>,
Expand All @@ -698,7 +706,7 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
else uuid = this.redirectToPlainSplitIfNeeded(uuid, event)

if (isTabLayoutModificationResponse(event.response)) {
const updatedResponse = this.onTabLayoutModificationRequest(event.response, uuid)
const updatedResponse = await this.onTabLayoutModificationRequest(event.response, uuid)
if (updatedResponse) {
event.response = updatedResponse
}
Expand Down Expand Up @@ -872,12 +880,31 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
}

/** Split the view */
private onSplit(request: TabLayoutModificationResponse<NewSplitRequest>, sbuuid: string) {
private async onSplit(request: TabLayoutModificationResponse<NewSplitRequest>, sbuuid: string) {
const nTerminals = this.state.splits.length

if (nTerminals === MAX_TERMINALS) {
return new Error(strings('No more splits allowed'))
} else {
if (request.spec.options.cmdline && (request.spec.options.if || request.spec.options.ifnot)) {
const thisSplitIdx = this.findSplit(this.state, sbuuid)
const thisSplit = this.state.splits[thisSplitIdx]
const respIf = !request.spec.options.if
? true
: await thisSplit.facade.REPL.qexec<boolean>(request.spec.options.if).catch(() => false)
const respIfNot = !request.spec.options.ifnot
? true
: !(await thisSplit.facade.REPL.qexec<boolean>(request.spec.options.ifnot).catch(() => true))
if (!respIf || !respIfNot) {
const { cmdline } = request.spec.options
const mainSplit =
this.state.splits.find((split, idx) => !this.isMiniSplit(split, idx) && idx !== thisSplitIdx) || thisSplit
request.spec.options.cmdline = undefined // null this out, since we got it!
mainSplit.facade.REPL.pexec(cmdline)
return
}
}

const newScrollback = this.scrollback(undefined, request.spec.options)

this.setState(({ splits }) => {
Expand Down Expand Up @@ -1039,6 +1066,8 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
ref['facade'] = scrollback.facade
scrollback.facade.getSize = getSize.bind(ref)

scrollback.facade.splitCount = () => this.state.splits.length

scrollback.facade.scrollToBottom = () => {
ref.scrollTop = ref.scrollHeight
}
Expand Down
3 changes: 3 additions & 0 deletions plugins/plugin-client-common/src/controller/split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export function debug(args: Arguments) {
*/
export default function split(args?: Arguments<CommandLineOptions>): TabLayoutModificationResponse<NewSplitRequest> {
const options: Options = {
if: args.parsedOptions.if,
ifnot: args.parsedOptions.ifnot,
index: args.parsedOptions.index,
cmdline: args.parsedOptions.cmdline,
inverseColors: args.parsedOptions.inverse
}

Expand Down

0 comments on commit 5d3f032

Please sign in to comment.