Skip to content

Commit

Permalink
feat: add capability to show welcome widget to new users in Terminal
Browse files Browse the repository at this point in the history
Fixes #4990
Fixes #5007
  • Loading branch information
myan9 authored and starpit committed Jun 26, 2020
1 parent 6f9ba7b commit 5778a93
Show file tree
Hide file tree
Showing 18 changed files with 199 additions and 111 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -65,7 +65,7 @@ jobs:

- env: N="API" CLIENT="test" MOCHA_RUN_TARGET="webpack" LAYERS="response" CODECOV_OF_PRESCAN=true
- env: N="API" CLIENT="test" MOCHA_RUN_TARGET="electron" LAYERS="response"
- env: N="bottom-input" CLIENT="alternate" MOCHA_RUN_TARGET="webpack" LAYERS="bottom-input"
- env: N="bottom-input" CLIENT="alternate" MOCHA_RUN_TARGET="webpack" LAYERS="bottom-input" BOTTOM_INPUT_MODE=true

# CLIENT=default | os=Darwin | TARGET=electron | core, core-support, editor, core-support2
- os: osx
Expand Down
5 changes: 4 additions & 1 deletion packages/test/src/api/cli.ts
Expand Up @@ -68,7 +68,9 @@ export const command = async (
if (process.env.BOTTOM_INPUT_MODE) await app.client.waitForExist(Selectors.BOTTOM_PROMPT_BLOCK, timeout - 5000)
if (!noFocus) return grabFocus(app)
})
.then(() => app.client.getAttribute(block, 'data-input-count'))
.then(() =>
app.client.getAttribute(process.env.BOTTOM_INPUT_MODE ? Selectors.BOTTOM_PROMPT_BLOCK : block, 'data-input-count')
)
.then(async count => {
if (!noCopyPaste && cmd.length > 1) {
// use the clipboard for a fast path
Expand Down Expand Up @@ -120,6 +122,7 @@ export const waitForSession = async (ctx: Common.ISuite, noProxySessionWait = fa
// wait for the proxy session to be established
try {
await ctx.app.client.waitForExist(`${Selectors.CURRENT_TAB}.kui--session-init-done`)
await ctx.app.client.waitForVisible(Selectors.WELCOME_BLOCK)
} catch (err) {
throw new Error('error waiting for proxy session init')
}
Expand Down
2 changes: 1 addition & 1 deletion packages/test/src/api/repl-expect.ts
Expand Up @@ -172,7 +172,7 @@ export const blank = (res: AppAndCount) => blankWithOpts()(res)
export const consoleToBeClear = (app: Application) => {
return app.client.waitUntil(async () => {
return app.client.elements(Selectors.PROMPT_BLOCK).then(elements => elements.value.length === 1)
})
}, waitTimeout)
}

/** as long as its ok, accept anything */
Expand Down
5 changes: 3 additions & 2 deletions packages/test/src/api/selectors.ts
Expand Up @@ -8,6 +8,7 @@ export const SIDECAR_FULLSCREEN = `${CURRENT_TAB} .kui--sidecar.visible.maximize
export const TERMINAL_WITH_SIDECAR_VISIBLE = `${CURRENT_TAB} .repl.sidecar-visible`
const _PROMPT_BLOCK = '.repl-block'
export const PROMPT_BLOCK = `${CURRENT_TAB} .repl ${_PROMPT_BLOCK}`
export const WELCOME_BLOCK = `${PROMPT_BLOCK} .kui--repl-message.kui--session-init-done`
export const BOTTOM_PROMPT_BLOCK = `${CURRENT_TAB} .kui--input-stripe .repl-block`
export const BOTTOM_PROMPT = `${BOTTOM_PROMPT_BLOCK} input`
export const STATUS_STRIPE_BLOCK = '.kui--status-stripe .kui--input-stripe .repl-block'
Expand Down Expand Up @@ -103,8 +104,8 @@ export const OUTPUT_N_PTY = (N: number) => OUTPUT_N_STREAMING(N)
export const PROMPT_BLOCK_LAST = `${PROMPT_BLOCK}:nth-last-child(2)`
export const PROMPT_BLOCK_FINAL = `${PROMPT_BLOCK}:nth-last-child(1)`
export const OVERFLOW_MENU = '.kui--repl-block-right-element.kui--toolbar-button-with-icon'
export const PROMPT_BLOCK_LAST_MENU = `${PROMPT_BLOCK_LAST} ${OVERFLOW_MENU}`
export const BLOCK_REMOVE_BUTTON = `${OVERFLOW_MENU} button[data-mode="Remove"]`
export const PROMPT_BLOCK_MENU = (N: number) => `${PROMPT_BLOCK_N(N)} ${OVERFLOW_MENU}`
export const BLOCK_REMOVE_BUTTON = `${OVERFLOW_MENU} button[data-mode="Remove"]` // in carbon, this is a global
export const PROMPT_LAST = `${PROMPT_BLOCK_LAST} .repl-input-element`
export const PROMPT_FINAL = `${PROMPT_BLOCK_FINAL} .repl-input-element`
export const OUTPUT_LAST = `${PROMPT_BLOCK_LAST} .repl-result`
Expand Down
12 changes: 7 additions & 5 deletions plugins/plugin-bash-like/web/scss/xterm.scss
@@ -1,4 +1,4 @@
@import "~xterm/css/xterm.css";
@import '~xterm/css/xterm.css';

/* Explanation for width hacks:
see https://github.com/xtermjs/xterm.js/pull/2067
Expand Down Expand Up @@ -39,8 +39,10 @@ disabled see https://github.com/IBM/kui/issues/3939
height: auto !important;
}

/* alt buffer mode */
&.xterm-alt-buffer-mode, .xterm-alt-buffer-mode { /* no-splitTerminal and splitTerminal variants */
/* alt buffer mode */
&.visible.xterm-alt-buffer-mode,
.xterm-alt-buffer-mode {
/* no-splitTerminal and splitTerminal variants */
.xterm-container .xterm-rows.xterm-focus .xterm-cursor.xterm-cursor-block {
background-color: var(--color-base08);
color: var(--color-base00);
Expand Down Expand Up @@ -265,7 +267,7 @@ disabled see https://github.com/IBM/kui/issues/3939
.xterm-is-wrapped-with-prefix-break:before {
/* see https://github.com/IBM/kui/issues/1605 */
display: block;
content: "";
content: '';

/* see https://github.com/IBM/kui/issues/2681 */
height: 0;
Expand All @@ -287,6 +289,6 @@ disabled see https://github.com/IBM/kui/issues/3939
}

/* selection */
[kui-theme-style="light"] .xterm-container .xterm-screen .xterm-selection div {
[kui-theme-style='light'] .xterm-container .xterm-screen .xterm-selection div {
background-color: rgba(0, 0, 0, 0.3);
}
12 changes: 6 additions & 6 deletions plugins/plugin-client-alternate/src/test/bottom-input/new-tab.ts
Expand Up @@ -26,24 +26,24 @@ describe('core new tab switch tabs', function(this: Common.ISuite) {
CLI.command('tab new', this.app)
.then(() => this.app.client.waitForVisible(Selectors.TAB_SELECTED_N(2)))
.then(() => CLI.waitForSession(this)) // should have an active repl
.catch(Common.oops(this)))
.catch(Common.oops(this, true)))

it(`switch back to first tab via command`, () =>
CLI.command('tab switch 1', this.app)
.then(() => this.app.client.waitForVisible(Selectors.TAB_SELECTED_N(1)))
.catch(Common.oops(this)))
.catch(Common.oops(this, true)))

it(`switch back to second tab via command`, () =>
CLI.command('tab switch 2', this.app)
.then(() => this.app.client.waitForVisible(Selectors.TAB_SELECTED_N(2)))
.catch(Common.oops(this)))
.catch(Common.oops(this, true)))

it('should close tab via "tab close" command', () =>
CLI.command('tab close', this.app)
.then(() => this.app.client.waitForExist(Selectors.TAB_N(2), 5000, true))
.then(() => this.app.client.waitForVisible(Selectors.TAB_SELECTED_N(1)))
.then(() => CLI.waitForRepl(this.app)) // should have an active repl
.catch(Common.oops(this)))
.catch(Common.oops(this, true)))
})

describe('core new tab from pty active tab via button click', function(this: Common.ISuite) {
Expand All @@ -56,11 +56,11 @@ describe('core new tab from pty active tab via button click', function(this: Com
.then(() => this.app.client.click(tabButtonSelector))
.then(() => this.app.client.waitForVisible(Selectors.TAB_SELECTED_N(2)))
.then(() => CLI.waitForSession(this)) // should have an active repl
.catch(Common.oops(this)))
.catch(Common.oops(this, true)))

it('should report proper version', () =>
CLI.command('version', this.app)
.then(ReplExpect.okWithCustom({ expect: Common.expectedVersion }))
.then(() => this.app.client.waitForVisible(Selectors.TAB_SELECTED_N(2)))
.catch(Common.oops(this)))
.catch(Common.oops(this, true)))
})
3 changes: 2 additions & 1 deletion plugins/plugin-client-common/src/components/Client/Kui.tsx
Expand Up @@ -119,7 +119,8 @@ export class Kui extends React.PureComponent<Props, State> {

private defaultFeatureFlag() {
return {
sidecarName: 'breadcrumb'
sidecarName: 'breadcrumb',
showWelcomeMax: -1
}
}

Expand Down
18 changes: 0 additions & 18 deletions plugins/plugin-client-common/src/components/Client/TabContent.tsx
Expand Up @@ -212,23 +212,6 @@ export default class TabContent extends React.PureComponent<Props, State> {
return <Loading description={strings('Please wait while we connect to your cloud')} />
}

private sessionInitDoneMessage() {
return (
this.state.showSessionInitDone &&
this.state.sidecarWidth === Width.Closed && (
<KuiContext.Consumer>
{config =>
config.loadingDone && (
<div className="kui--repl-message kui--session-init-done">
<span className="repl-block">{config.loadingDone(this.state.tab.REPL)}</span>
</div>
)
}
</KuiContext.Consumer>
)
)
}

private terminal() {
if (this.state.sessionInit !== 'Done') {
return (
Expand All @@ -247,7 +230,6 @@ export default class TabContent extends React.PureComponent<Props, State> {
} else {
return (
<React.Fragment>
{this.sessionInitDoneMessage()}
<KuiContext.Consumer>
{config => (
<ScrollableTerminal
Expand Down
Expand Up @@ -29,6 +29,15 @@ type FeatureFlags = {

/** [Optional] automatically pin watchable command ouptut to the WatchPane? */
enableWatcherAutoPin?: boolean

/**
* [Optional] maximum number of times to show `loadingDone` to users
* if set to -1, always show welcome;
* if not 0, not show welcome
* default: -1
*
*/
showWelcomeMax?: number
}

export default FeatureFlags
Expand Up @@ -35,9 +35,14 @@ type WithState<S extends BlockState> = { state: S }
type WithResponse<R extends ScalarResponse> = { response: R } & WithStartTime
type WithValue = { value: string }
type withPin = { isPinned: boolean }
type WithAnnouncement = { isAnnouncement: boolean }

/** The canonical types of Blocks, which mix up the Traits as needed */
type ActiveBlock = WithState<BlockState.Active> & WithCWD & Partial<WithValue>
export type AnnouncementBlock = WithState<BlockState.ValidResponse> &
WithResponse<ScalarResponse> &
WithCWD &
WithAnnouncement
type EmptyBlock = WithState<BlockState.Empty> & WithCWD
type ErrorBlock = WithState<BlockState.Error> & WithCommand & WithResponse<Error> & WithUUID
type OkBlock = WithState<BlockState.ValidResponse> & WithCommand & WithResponse<ScalarResponse> & WithUUID
Expand All @@ -48,7 +53,8 @@ type CancelledBlock = WithState<BlockState.Cancelled> & WithCWD & WithCommand &
export type FinishedBlock = OkBlock | ErrorBlock | CancelledBlock | EmptyBlock

// A Block is one of the canonical types
export type BlockModel = (ProcessingBlock | FinishedBlock | CancelledBlock | ActiveBlock) & Partial<withPin>
export type BlockModel = (ProcessingBlock | FinishedBlock | CancelledBlock | ActiveBlock | AnnouncementBlock) &
Partial<withPin>
export default BlockModel

/** Capture the current working directory */
Expand Down Expand Up @@ -93,8 +99,13 @@ export function hasCommand(block: BlockModel & Partial<WithCommand>): block is B
return !isActive(block) && !isEmpty(block)
}

export function isAnnouncement(block: BlockModel): block is AnnouncementBlock {
const blockModel = block as AnnouncementBlock
return blockModel.state === BlockState.ValidResponse && blockModel.isAnnouncement === true
}

export function hasUUID(block: BlockModel & Partial<WithUUID>): block is BlockModel & Required<WithUUID> {
return !isActive(block) && !isEmpty(block)
return !isActive(block) && !isEmpty(block) && !isAnnouncement(block)
}

export function hasValue(block: BlockModel): block is BlockModel & Required<WithValue> {
Expand All @@ -110,6 +121,17 @@ export function Active(initialValue?: string): ActiveBlock {
}
}

/** Transform to AnnouncementBlock */
export function Announcement(response: ScalarResponse): AnnouncementBlock {
return {
response,
isAnnouncement: true,
startTime: new Date(),
cwd: cwd(),
state: BlockState.ValidResponse
}
}

/** Transform to Processing */
export function Processing(block: BlockModel, command: string, execUUID: string): ProcessingBlock {
return {
Expand Down
Expand Up @@ -19,7 +19,7 @@ import { Tab as KuiTab, eventChannelUnsafe } from '@kui-shell/core'

import Input, { InputOptions } from './Input'
import Output from './Output'
import { BlockModel, isActive, isEmpty, isFinished, isProcessing, hasUUID } from './BlockModel'
import { BlockModel, isActive, isEmpty, isFinished, isProcessing, isAnnouncement, hasUUID } from './BlockModel'

export type BlockViewTraits = {
isPinned?: boolean
Expand Down Expand Up @@ -137,11 +137,14 @@ export default class Block extends React.PureComponent<Props, State> {
<div
className={'repl-block ' + this.props.model.state.toString()}
data-pinned={this.props.isPinned || undefined}
data-announcement={isAnnouncement(this.props.model) || undefined}
data-uuid={hasUUID(this.props.model) && this.props.model.execUUID}
data-input-count={this.props.idx}
ref={c => this.setState({ _block: c })}
>
{isActive(this.props.model) || isEmpty(this.props.model) ? (
{isAnnouncement(this.props.model) ? (
this.output()
) : isActive(this.props.model) || isEmpty(this.props.model) ? (
this.input()
) : (
<React.Fragment>
Expand Down
Expand Up @@ -24,6 +24,7 @@ import {
ScalarResponse,
Tab as KuiTab,
ExecOptions,
ExecType,
isPopup,
CommandStartEvent,
CommandCompleteEvent,
Expand All @@ -37,6 +38,7 @@ import {
Active,
Finished,
FinishedBlock,
Announcement,
Cancelled,
Processing,
isActive,
Expand All @@ -57,6 +59,9 @@ const MAX_TERMINALS = 2
/** Hard limit on the number of Pinned splits */
const MAX_PINNED = 3

/** Remember the welcomed count in localStorage, using this key */
const NUM_WELCOMED = 'kui-shell.org/ScrollableTerminal/NumWelcomed'

export interface TerminalOptions {
noActiveInput?: boolean
}
Expand Down Expand Up @@ -143,10 +148,57 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State

this.state = {
focusedIdx: 0,
splits: [this.scrollback()]
splits: [this.scrollbackWithWelcome()]
}
}

/** add welcome blocks at the top of scrollback */
private scrollbackWithWelcome() {
const scrollback = this.scrollback()
const welcomeMax = this.props.config.showWelcomeMax

if (this.props.config.loadingDone && welcomeMax !== undefined) {
const welcomed = parseInt(localStorage.getItem(NUM_WELCOMED)) || 0

if ((welcomeMax === -1 || welcomed < welcomeMax) && this.props.config.loadingDone) {
const announcement = this.props.config.loadingDone(this.props.tab.REPL)
if (announcement) {
eventBus.emitCommandComplete({
tab: this.props.tab,
command: 'welcome',
argvNoOptions: ['welcome'],
parsedOptions: {},
execOptions: {},
execUUID: '',
execType: ExecType.TopLevel,
cancelled: false,
echo: true,
evaluatorOptions: {},
response: { react: announcement },
responseType: 'ScalarResponse'
})
}
const welcomeBlocks: BlockModel[] = !announcement
? []
: [
Announcement({
react: this.props.config.loadingDone && (
<div className="kui--repl-message kui--session-init-done">{announcement}</div>
)
})
]

scrollback.blocks = welcomeBlocks.concat(scrollback.blocks)

if (welcomeMax !== -1) {
localStorage.setItem(NUM_WELCOMED, (welcomed + 1).toString())
}
}
}

return scrollback
}

private allocateUUIDForScrollback(forSplit = false) {
if (forSplit || (this.props.config.splitTerminals && !isPopup())) {
return `${this.props.uuid}_${uuid()}`
Expand Down
Expand Up @@ -82,18 +82,16 @@ export default class PatternflyCard extends React.PureComponent<Props, State> {

/** card actions, icon and custom header node will be situated in Card Head */
private header() {
if (this.props.header || this.props.actions || this.props.icon) {
return (
<CardHeader>
<CardHeaderMain>{this.props.header || (this.props.icon && this.icon())}</CardHeaderMain>
{this.props.actions && this.cardActions()}
<div>
{this.props.titleInHeader && this.title()}
{this.props.bodyInHeader && this.body()}
</div>
</CardHeader>
)
}
return (
<CardHeader>
<CardHeaderMain>{this.props.header || (this.props.icon && this.icon())}</CardHeaderMain>
{this.props.actions && this.cardActions()}
<div>
{this.props.titleInHeader && this.title()}
{this.props.bodyInHeader && this.body()}
</div>
</CardHeader>
)
}

private title() {
Expand Down

0 comments on commit 5778a93

Please sign in to comment.