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

Commit a1ff1a4

Browse files
committed
feat: update ls to allow for presenting content from more limited VFS's such as tutorials
this also updates TableCell.tsx to allow for Markdown content in cells. Fixes #5497
1 parent 128ec92 commit a1ff1a4

File tree

7 files changed

+78
-47
lines changed

7 files changed

+78
-47
lines changed

packages/core/src/webapp/models/table.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ export enum TableStyle {
130130
export class Table<RowType extends Row = Row> {
131131
body: RowType[]
132132

133-
// type?: string
133+
/** Markdown cells? */
134+
markdown?: boolean
134135

135136
/** Column index to be interpreted as a status column */
136137
statusColumnIdx?: number

plugins/plugin-bash-like/fs/src/lib/ls.ts

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ function cssOf(glob: GlobStats): string {
105105
* Decorate the name according to its nature
106106
*
107107
*/
108-
function nameOf(glob: GlobStats): string {
109-
return `${glob.nameForDisplay}${
108+
function nameOf(glob: GlobStats, wide: boolean): string {
109+
return `${wide ? glob.nameForDisplay : glob.name}${
110110
glob.dirent.isDirectory
111111
? !glob.nameForDisplay.endsWith('/')
112112
? '/'
@@ -136,25 +136,35 @@ function formatGid(entry: GlobStats) {
136136
return entry.stats.gid.toString()
137137
}
138138

139-
function attrs(entry: GlobStats, args: Arguments<LsOptions>) {
139+
function attrs(
140+
entry: GlobStats,
141+
args: Arguments<LsOptions>,
142+
hasPermissions: boolean,
143+
hasSize: boolean,
144+
hasUid: boolean,
145+
hasGid: boolean,
146+
hasMtime: boolean
147+
) {
140148
// const language = withLanguage(args.execOptions).language
141149

142150
const wide = args.parsedOptions.l
143-
const perms = wide ? [{ value: formatPermissions(entry), outerCSS: outerCSSSecondary }] : []
144-
const uid = wide ? [{ value: formatUid(entry), outerCSS: outerCSSSecondary, css: cssSecondary }] : []
145-
const gid = wide ? [{ value: formatGid(entry), outerCSS: outerCSSSecondary, css: cssSecondary }] : []
146-
const size = wide
147-
? [{ value: prettyBytes(entry.stats.size).replace(/\s/g, ''), outerCSS: `${outerCSSSecondary} text-right` }]
148-
: []
149-
const lastMod = wide
150-
? [
151-
{
152-
value: prettyTime(entry.stats.mtimeMs),
153-
outerCSS: outerCSSLesser,
154-
css: `${cssLesser} ${cssSecondary} pre-wrap`
155-
}
156-
]
157-
: []
151+
const perms = wide && hasPermissions ? [{ value: formatPermissions(entry), outerCSS: outerCSSSecondary }] : []
152+
const uid = wide && hasUid ? [{ value: formatUid(entry), outerCSS: outerCSSSecondary, css: cssSecondary }] : []
153+
const gid = wide && hasGid ? [{ value: formatGid(entry), outerCSS: outerCSSSecondary, css: cssSecondary }] : []
154+
const size =
155+
wide && hasSize
156+
? [{ value: prettyBytes(entry.stats.size).replace(/\s/g, ''), outerCSS: `${outerCSSSecondary} text-right` }]
157+
: []
158+
const lastMod =
159+
wide && hasMtime
160+
? [
161+
{
162+
value: prettyTime(entry.stats.mtimeMs),
163+
outerCSS: outerCSSLesser,
164+
css: `${cssLesser} ${cssSecondary} pre-wrap`
165+
}
166+
]
167+
: []
158168

159169
return perms
160170
.concat(uid)
@@ -171,12 +181,18 @@ function toTable(entries: GlobStats[], args: Arguments<LsOptions>): HTMLElement
171181
const rev = args.parsedOptions.r ? -1 : 1
172182
const sorter = args.parsedOptions.S ? bySize(rev) : args.parsedOptions.t ? byTime(rev) : byLex(rev)
173183

184+
const hasPermissions = entries.some(_ => _.dirent && _.dirent.permissions)
185+
const hasSize = entries.some(_ => _.stats && _.stats.size)
186+
const hasUid = entries.some(_ => (_.dirent && _.dirent.username) || (_.stats && _.stats.uid >= 0))
187+
const hasGid = entries.some(_ => _.stats && _.stats.gid >= 0)
188+
const hasMtime = entries.some(_ => _.stats && _.stats.mtimeMs)
189+
174190
const body = entries.sort(sorter).map(_ => ({
175-
name: nameOf(_),
191+
name: nameOf(_, args.parsedOptions.l),
176192
css: cssOf(_),
177193
onclickExec: 'pexec' as const,
178194
onclick: `${_.dirent.isDirectory ? 'ls' : 'open'} ${args.REPL.encodeComponent(_.path)}`,
179-
attributes: attrs(_, args)
195+
attributes: attrs(_, args, hasPermissions, hasSize, hasUid, hasGid, hasMtime)
180196
}))
181197

182198
if (!args.parsedOptions.l) {
@@ -210,11 +226,11 @@ function toTable(entries: GlobStats[], args: Arguments<LsOptions>): HTMLElement
210226
container.appendChild(frag)
211227
return container
212228
} else {
213-
const perms = [{ value: 'Permissions', outerCSS: outerCSSSecondary }]
214-
const uid = [{ value: 'User', outerCSS: outerCSSSecondary }]
215-
const gid = [{ value: 'Group', outerCSS: outerCSSSecondary }]
216-
const size = [{ value: 'Size', outerCSS: `${outerCSSSecondary} text-right` }]
217-
const lastMod = [{ value: 'Last Modified', outerCSS: outerCSSLesser, css: cssLesser }]
229+
const perms = hasPermissions ? [{ value: 'Permissions', outerCSS: outerCSSSecondary }] : []
230+
const uid = hasUid ? [{ value: 'User', outerCSS: outerCSSSecondary }] : []
231+
const gid = hasGid ? [{ value: 'Group', outerCSS: outerCSSSecondary }] : []
232+
const size = hasSize ? [{ value: 'Size', outerCSS: `${outerCSSSecondary} text-right` }] : []
233+
const lastMod = hasMtime ? [{ value: 'Last Modified', outerCSS: outerCSSLesser, css: cssLesser }] : []
218234

219235
const header = {
220236
name: 'Name',
@@ -228,6 +244,7 @@ function toTable(entries: GlobStats[], args: Arguments<LsOptions>): HTMLElement
228244
return {
229245
header,
230246
body,
247+
markdown: true,
231248
noSort: true,
232249
noEntityColors: true
233250
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as prettyPrintDuration from 'pretty-ms'
1919
import { TableCell, DataTableCell } from 'carbon-components-react'
2020
import { Table as KuiTable, Cell as KuiCell, Row as KuiRow, Tab, REPL } from '@kui-shell/core'
2121

22+
import Markdown from '../Markdown'
2223
import ErrorCell from './ErrorCell'
2324

2425
/**
@@ -114,7 +115,7 @@ export default function renderCell(table: KuiTable, kuiRow: KuiRow, justUpdated:
114115
{/red-background/.test(css) ? <ErrorCell /> : undefined}
115116
</span>
116117
)}
117-
<span className="kui--cell-inner-text">{innerText}</span>
118+
<span className="kui--cell-inner-text">{table.markdown ? <Markdown source={innerText} /> : innerText}</span>
118119
</span>
119120
</TableCell>
120121
)

plugins/plugin-client-common/web/scss/components/Table/tables.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ body .bx--data-table-container {
4040
.bx--data-table {
4141
font-family: var(--font-sans-serif);
4242

43+
/* markdown */
44+
p {
45+
padding: 0;
46+
font-size: inherit;
47+
line-height: inherit;
48+
font-weight: inherit;
49+
}
50+
4351
/* td [data-key='NAME'],
4452
td .kui--table-cell-is-name {
4553
font-weight: 700;

plugins/plugin-client-default/config.d/about.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
}
1414
],
1515
"links": [
16-
{ "label": "Home Page", "href": "http://kui.tools" },
1716
{ "label": "Github", "href": "https://github.com/IBM/kui" },
1817
{ "label": "Bugs", "href": "https://github.com/IBM/kui/issues/new" },
1918
{ "label": "Kubectl Help", "command": "kubectl --help" },
20-
{ "label": "API Resources", "command": "kubectl api-resources" }
19+
{ "label": "API Resources", "command": "kubectl api-resources" },
20+
{ "label": "Kubernetes Notebooks", "command": "ls -l /kui/kubernetes" }
2121
]
2222
}

plugins/plugin-core-support/src/lib/cmds/replay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
* Schema for a serialized snapshot of the Inputs and Outputs of
3131
* command executions.
3232
*/
33-
interface SerializedSnapshot {
33+
export interface SerializedSnapshot {
3434
apiVersion: 'kui-shell/v1'
3535
kind: 'Snapshot'
3636
spec: Snapshot

plugins/plugin-core-support/src/tutorials/vfs/index.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import TrieSearch from 'trie-search'
1818
import { basename, dirname, join } from 'path'
1919
import { Arguments, CodedError, flatten } from '@kui-shell/core'
2020
import { FStat, VFS, mount } from '@kui-shell/plugin-bash-like/fs'
21-
import { productName } from '@kui-shell/client/config.d/name.json'
21+
22+
import { SerializedSnapshot } from '../../lib/cmds/replay'
2223

2324
interface Tutorial {
2425
name: string
@@ -28,23 +29,25 @@ interface Tutorial {
2829
nameForDisplay: string
2930
}
3031

31-
interface Entry {
32+
interface BaseEntry {
3233
mountPath: string
3334
}
3435

35-
type Directory = Entry
36+
type Directory = BaseEntry
3637

37-
interface Leaf extends Entry {
38-
data: Record<string, any> // FIXME snapshot type
38+
interface Leaf extends BaseEntry {
39+
data: SerializedSnapshot
3940
}
4041

41-
function isDirectory(entry: Entry): entry is Directory {
42-
return (entry as Leaf).data === undefined
42+
type Entry = Leaf | Directory
43+
44+
function isLeaf(entry: Entry): entry is Leaf {
45+
return (entry as Leaf).data !== undefined
4346
}
4447

45-
const uid = 0
46-
const gid = 0
47-
const username = productName
48+
const uid = -1
49+
const gid = -1
50+
const username = ''
4851

4952
class TutorialVFS implements VFS {
5053
public readonly mountPath = '/kui'
@@ -83,13 +86,14 @@ class TutorialVFS implements VFS {
8386
}
8487

8588
private enumerate({ entries }: { entries: Entry[] }) {
86-
return entries.map(mount => {
89+
return entries.map((mount: Entry) => {
8790
const name = basename(mount.mountPath)
88-
const isFile = !isDirectory(mount)
91+
const nameForDisplay = isLeaf(mount) ? mount.data.spec.title || mount.data.spec.description || name : name
92+
const isDir = !isLeaf(mount)
8993

9094
return {
9195
name,
92-
nameForDisplay: name,
96+
nameForDisplay,
9397
path: mount.mountPath,
9498
stats: {
9599
size: 0,
@@ -99,8 +103,8 @@ class TutorialVFS implements VFS {
99103
gid
100104
},
101105
dirent: {
102-
isFile,
103-
isDirectory: !isFile,
106+
isFile: !isDir,
107+
isDirectory: isDir,
104108
isSymbolicLink: false,
105109
isSpecial: false,
106110
isExecutable: false,
@@ -178,8 +182,8 @@ class TutorialVFS implements VFS {
178182
viewer: 'replay --new-window',
179183
filepath: entry.mountPath,
180184
fullpath: entry.mountPath,
181-
isDirectory: isDirectory(entry),
182-
data: withData && !isDirectory(entry) ? JSON.stringify(entry.data, undefined, 2) : undefined
185+
isDirectory: !isLeaf(entry),
186+
data: withData && isLeaf(entry) ? JSON.stringify(entry.data, undefined, 2) : undefined
183187
}
184188
}
185189
}

0 commit comments

Comments
 (0)