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

Commit 47735d0

Browse files
mra-ruizstarpit
authored andcommitted
feat: toggling edit mode using CLI command
1 parent baf8823 commit 47735d0

File tree

10 files changed

+340
-71
lines changed

10 files changed

+340
-71
lines changed

packages/core/src/core/events.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,11 @@ class WriteEventBus extends EventBusBase {
160160
this.eventBus.emit('/status-stripe/change', evt)
161161
}
162162

163-
public emitWithTabId(channel: '/tab/offline' | '/tab/close/request', tabId: string, tab?: Tab): void {
163+
public emitWithTabId(
164+
channel: '/tab/offline' | '/tab/close/request' | '/kui/tab/edit/toggle',
165+
tabId: string,
166+
tab?: Tab
167+
): void {
164168
this.eventBus.emit(`${channel}/${tabId}`, tabId, tab)
165169
}
166170
}
@@ -347,23 +351,23 @@ class ReadEventBus extends WriteEventBus {
347351
}
348352

349353
public onWithTabId(
350-
channel: '/tab/offline' | '/tab/close/request',
354+
channel: '/tab/offline' | '/tab/close/request' | '/kui/tab/edit/toggle',
351355
tabId: string,
352356
listener: (tabId: string, tab: Tab) => void
353357
): void {
354358
this.eventBus.on(`${channel}/${tabId}`, listener)
355359
}
356360

357361
public offWithTabId(
358-
channel: '/tab/offline' | '/tab/close/request',
362+
channel: '/tab/offline' | '/tab/close/request' | '/kui/tab/edit/toggle',
359363
tabId: string,
360364
listener: (tabId: string, tab: Tab) => void
361365
): void {
362366
this.eventBus.off(`${channel}/${tabId}`, listener)
363367
}
364368

365369
public onceWithTabId(
366-
channel: '/tab/offline' | '/tab/close/request',
370+
channel: '/tab/offline' | '/tab/close/request' | '/kui/tab/edit/toggle',
367371
tabId: string,
368372
listener: (tabId: string, tab: Tab) => void
369373
): void {

plugins/plugin-client-common/src/components/Client/MutabilityContext.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,9 @@ export function initializeState(model: TabModel['_snapshot']): MutabilityState {
4545
}
4646
}
4747

48+
export function toggleReadOnlyBit(state: MutabilityState): MutabilityState {
49+
return { editable: !state.editable, executable: state.executable }
50+
}
51+
4852
/** Context variable to keep track of tab content viewing mode: edit or read only */
4953
export const MutabilityContext = React.createContext(defaultState)

plugins/plugin-client-common/src/components/Client/TabContent.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const Confirm = React.lazy(() => import('../Views/Confirm'))
2323
import getSize from '../Views/Terminal/getSize'
2424
import SessionInitStatus from './SessionInitStatus'
2525
import ScrollableTerminal, { TerminalOptions } from '../Views/Terminal/ScrollableTerminal'
26-
import { MutabilityState, MutabilityContext, initializeState } from './MutabilityContext'
26+
import { initializeState, MutabilityContext, MutabilityState, toggleReadOnlyBit } from './MutabilityContext'
2727

2828
type Cleaner = () => void
2929

@@ -86,6 +86,12 @@ export default class TabContent extends React.PureComponent<Props, State> {
8686
/** switching back or away from this tab */
8787
private activateHandlers: ((isActive: boolean) => void)[] = []
8888

89+
private readonly toggleEditMode = () => {
90+
this.setState(state => ({
91+
mutability: toggleReadOnlyBit(state.mutability)
92+
}))
93+
}
94+
8995
public constructor(props: Props) {
9096
super(props)
9197

@@ -131,6 +137,10 @@ export default class TabContent extends React.PureComponent<Props, State> {
131137
const onOffline = this.onOffline.bind(this)
132138
eventBus.onWithTabId('/tab/offline', this.props.uuid, onOffline)
133139
this.cleaners.push(() => eventBus.offWithTabId('/tab/offline', this.props.uuid, onOffline))
140+
141+
const onEditToggle = this.toggleEditMode
142+
eventBus.onWithTabId('/kui/tab/edit/toggle', this.props.uuid, onEditToggle)
143+
this.cleaners.push(() => eventBus.offWithTabId('/kui/tab/edit/toggle', this.props.uuid, onEditToggle))
134144
}
135145

136146
private async onOffline() {

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

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
hideOutput,
3535
hasUUID
3636
} from './BlockModel'
37+
import { MutabilityContext } from '../../../Client/MutabilityContext'
3738

3839
export type BlockViewTraits = {
3940
/** number of splits currently in this tab */
@@ -234,40 +235,45 @@ export default class Block extends React.PureComponent<Props, State> {
234235

235236
return (
236237
(!this.props.noActiveInput || !isActive(this.props.model)) && (
237-
<li
238-
className={'repl-block ' + (hideOut ? '' : this.props.model.state.toString())}
239-
data-is-executale={this.props.isExecutable}
240-
data-is-section-break={this.props.isSectionBreak}
241-
data-in-sections={this.props.sectionIdx !== undefined || undefined}
242-
data-is-maximized={this.state.isMaximized || undefined}
243-
data-is-output-only={isOutputOnly(this.props.model) || undefined}
244-
data-is-empty={isEmpty(this.props.model) || undefined}
245-
data-announcement={isAnnouncement(this.props.model) || undefined}
246-
data-uuid={hasUUID(this.props.model) && this.props.model.execUUID}
247-
data-scrollback-uuid={this.props.uuid}
248-
data-input-count={this.props.idx}
249-
data-is-focused={this.props.isFocused || undefined}
250-
data-is-visible-in-minisplit={this.props.isVisibleInMiniSplit || undefined}
251-
data-is-replay={isReplay(this.props.model) || undefined}
252-
ref={c => this.setState({ _block: c })}
253-
tabIndex={isActive(this.props.model) ? -1 : 1}
254-
onClick={this.props.willFocusBlock}
255-
onFocus={this.props.onFocus}
256-
>
257-
<React.Fragment>
258-
{isLinkified(this.props.model) && <a id={this.props.model.link} />}
259-
{isAnnouncement(this.props.model) || isOutputOnly(this.props.model) ? (
260-
this.output()
261-
) : isActive(this.props.model) || isEmpty(this.props.model) ? (
262-
this.input()
263-
) : (
238+
<MutabilityContext.Consumer>
239+
{value => (
240+
<li
241+
className={'repl-block ' + (hideOut ? '' : this.props.model.state.toString())}
242+
data-is-executable={value.executable || undefined}
243+
data-is-editable={value.editable || undefined}
244+
data-is-section-break={this.props.isSectionBreak}
245+
data-in-sections={this.props.sectionIdx !== undefined || undefined}
246+
data-is-maximized={this.state.isMaximized || undefined}
247+
data-is-output-only={isOutputOnly(this.props.model) || undefined}
248+
data-is-empty={isEmpty(this.props.model) || undefined}
249+
data-announcement={isAnnouncement(this.props.model) || undefined}
250+
data-uuid={hasUUID(this.props.model) && this.props.model.execUUID}
251+
data-scrollback-uuid={this.props.uuid}
252+
data-input-count={this.props.idx}
253+
data-is-focused={this.props.isFocused || undefined}
254+
data-is-visible-in-minisplit={this.props.isVisibleInMiniSplit || undefined}
255+
data-is-replay={isReplay(this.props.model) || undefined}
256+
ref={c => this.setState({ _block: c })}
257+
tabIndex={isActive(this.props.model) ? -1 : 1}
258+
onClick={this.props.willFocusBlock}
259+
onFocus={this.props.onFocus}
260+
>
264261
<React.Fragment>
265-
{this.input()}
266-
{!hideOut && this.output()}
262+
{isLinkified(this.props.model) && <a id={this.props.model.link} />}
263+
{isAnnouncement(this.props.model) || isOutputOnly(this.props.model) ? (
264+
this.output()
265+
) : isActive(this.props.model) || isEmpty(this.props.model) ? (
266+
this.input()
267+
) : (
268+
<React.Fragment>
269+
{this.input()}
270+
{!hideOut && this.output()}
271+
</React.Fragment>
272+
)}
267273
</React.Fragment>
268-
)}
269-
</React.Fragment>
270-
</li>
274+
</li>
275+
)}
276+
</MutabilityContext.Consumer>
271277
)
272278
)
273279
}

plugins/plugin-client-common/src/test/core/mutabilityTests.ts renamed to plugins/plugin-client-common/src/test/core/notebooksReadOnly.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@ import { Common, CLI, ReplExpect, Selectors } from '@kui-shell/test'
1818

1919
const TIMEOUT = 1000
2020

21-
Common.localDescribe('tab content mutability', function(this: Common.ISuite) {
21+
Common.localDescribe('notebooks read only mode', function(this: Common.ISuite) {
2222
before(Common.before(this))
2323
after(Common.after(this))
2424

2525
const openNotebook = () => {
2626
it('should open a notebook using a CLI command', async () => {
2727
try {
28-
console.error(`inside it for trying to execute CLI command`)
2928
await CLI.command('replay /kui/welcome.json', this.app)
3029
.then(ReplExpect.error(127))
3130
.catch(Common.oops(this, true))

plugins/plugin-client-common/web/scss/components/Terminal/Notebook.scss

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,51 +28,55 @@ $margin-for-right-elements: calc(#{math.div(0.875 * $input-padding, $right-eleme
2828
}
2929
}
3030

31+
/** These are special stylings for read only mode, for example, we don't wantt to show all block actions */
3132
@include Scrollback {
32-
@include ReplayedBlock {
33-
@include show-block-action-buttons;
33+
@include FinishedBlock {
34+
@include NotEditableAndExecutable {
35+
@include show-block-action-buttons;
3436

35-
@include InputWrapper {
36-
padding-left: $input-padding;
37+
@include InputWrapper {
38+
padding-left: $input-padding;
3739

38-
@include Input {
39-
padding: #{0.875 * $input-padding} 0;
40-
padding-right: $input-padding;
41-
}
40+
@include Input {
41+
padding: #{0.875 * $input-padding} 0;
42+
padding-right: $input-padding;
43+
}
4244

43-
@include Spinner {
44-
margin: $margin-for-right-elements;
45-
}
45+
@include Spinner {
46+
margin: $margin-for-right-elements;
47+
}
4648

47-
/** Adjust and always show timestamp */
48-
@include BlockTimestamp {
49-
margin: $margin-for-right-elements;
50-
margin-right: 0.25em;
51-
}
52-
&:hover {
49+
/** Adjust and always show timestamp */
5350
@include BlockTimestamp {
54-
opacity: 1;
51+
margin: $margin-for-right-elements;
52+
margin-right: 0.25em;
53+
}
54+
&:hover {
55+
@include BlockTimestamp {
56+
opacity: 1;
57+
}
5558
}
5659
}
57-
}
5860

59-
@include Context {
60-
padding: #{0.875 * $input-padding} 0;
61-
}
61+
@include Context {
62+
padding: #{0.875 * $input-padding} 0;
63+
}
6264

63-
@include BlockActions {
64-
position: unset;
65-
background-color: transparent;
65+
@include BlockActions {
66+
position: unset;
67+
background-color: transparent;
6668

67-
&[data-has-been-rerun] {
68-
@include BlockAction {
69-
color: var(--color-gray);
69+
&[data-has-been-rerun] {
70+
@include BlockAction {
71+
color: var(--color-gray);
72+
}
7073
}
7174
}
72-
}
73-
@include BlockAction {
74-
font-size: 1.375em;
75-
color: var(--color-green);
75+
76+
@include BlockAction {
77+
font-size: 1.375em;
78+
color: var(--color-green);
79+
}
7680
}
7781
}
7882
}

plugins/plugin-client-common/web/scss/components/Terminal/_mixins.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,13 @@ $action-hover-delay: 210ms;
291291
}
292292
}
293293

294+
/** Use this mixin as a decorator of e.g. ReplayedBlock or FinishedBlock */
295+
@mixin NotEditableAndExecutable {
296+
&[data-is-executable]:not([data-is-editable]) {
297+
@content;
298+
}
299+
}
300+
294301
/** Hide the In[1] bits */
295302
@mixin HideIn {
296303
@include Block {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2021 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 {
18+
eventBus,
19+
getPrimaryTabId,
20+
getTab,
21+
KResponse,
22+
ParsedOptions,
23+
Registrar,
24+
StatusStripeChangeEvent
25+
} from '@kui-shell/core'
26+
27+
interface EditOptions extends ParsedOptions {
28+
'new-window': boolean
29+
'status-stripe': StatusStripeChangeEvent['type']
30+
}
31+
32+
/** Command registration */
33+
export default function(registrar: Registrar) {
34+
// register the `tab edit toggle` command
35+
registrar.listen<KResponse, EditOptions>('/tab/edit/toggle', ({ argvNoOptions }) => {
36+
if (argvNoOptions.length < 4) {
37+
throw new Error('Not enough arguments. Expected: tab edit toggle tabIndexNum')
38+
}
39+
40+
const index = parseInt(argvNoOptions[argvNoOptions.length - 1], 10)
41+
42+
if (isNaN(index)) {
43+
throw new Error(`4th argument is not a number. Expected type number`)
44+
} else {
45+
const tab = getTab(index)
46+
if (tab === undefined || tab === null) {
47+
throw new Error('Could not find tab with give index ' + index)
48+
}
49+
const uuid = getPrimaryTabId(tab)
50+
eventBus.emitWithTabId('/kui/tab/edit/toggle', uuid, tab)
51+
return 'Successfully toggled edit mode'
52+
}
53+
})
54+
}

plugins/plugin-core-support/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import clear from './lib/cmds/clear'
2323
import dopar from './lib/cmds/dopar'
2424
import watch from './lib/cmds/watch'
2525
import base64 from './lib/cmds/base64'
26+
import mutable from './lib/cmds/toggle-editability'
2627
import prompt from './lib/cmds/prompt'
2728
import replay from './lib/cmds/replay'
2829
import sleep from './lib/cmds/sleep'
@@ -46,6 +47,7 @@ export default async (commandTree: Registrar) => {
4647
commandTree.listen('/dopar', dopar),
4748
watch(commandTree),
4849
base64(commandTree),
50+
mutable(commandTree),
4951
prompt(commandTree),
5052
replay(commandTree),
5153
sleep(commandTree),

0 commit comments

Comments
 (0)