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

Commit 81050b1

Browse files
committed
feat: support for Description List UIs
part of #8058
1 parent efd20b2 commit 81050b1

File tree

38 files changed

+435
-93
lines changed

38 files changed

+435
-93
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export {
239239
FunctionThatProducesContent
240240
} from './models/mmr/content-types'
241241
export { ToolbarText, ToolbarAlert, isSupportedToolbarTextType } from './webapp/views/toolbar-text'
242+
export { default as DescriptionList, isDescriptionList } from './models/DescriptionList'
242243

243244
// low-level UI
244245
export { default as doCancel } from './webapp/cancel'
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 { ScalarResource } from './mmr/content-types'
18+
19+
export default interface DescriptionList {
20+
apiVersion: 'kui-shell/v1'
21+
kind: 'DescriptionList'
22+
23+
spec: {
24+
groups: {
25+
/** The term being described */
26+
term: string
27+
28+
/** The description of that term */
29+
description: number | boolean | string
30+
31+
/** Optional help details for the term */
32+
termHelp?: {
33+
title: string
34+
description: number | boolean | string
35+
}
36+
}[]
37+
}
38+
}
39+
40+
export function isDescriptionList(content: ScalarResource): content is DescriptionList {
41+
const dl = content as DescriptionList
42+
return (
43+
dl.apiVersion === 'kui-shell/v1' &&
44+
dl.kind === 'DescriptionList' &&
45+
typeof dl.spec === 'object' &&
46+
Array.isArray(dl.spec.groups) &&
47+
dl.spec.groups.length > 0 &&
48+
typeof dl.spec.groups[0].term === 'string' &&
49+
typeof dl.spec.groups[0].description !== 'undefined' &&
50+
(typeof dl.spec.groups[0].termHelp === 'undefined' ||
51+
(typeof dl.spec.groups[0].termHelp === 'object' &&
52+
typeof dl.spec.groups[0].termHelp.title === 'string' &&
53+
typeof dl.spec.groups[0].termHelp.description !== 'undefined'))
54+
)
55+
}

packages/core/src/models/mmr/content-types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ import { isHTML } from '../../util/types'
2323
import { ModeOrButton, Button } from './types'
2424
import { ToolbarText } from '../../webapp/views/toolbar-text'
2525
import { Editable } from '../editable'
26+
import DescriptionList, { isDescriptionList } from '../DescriptionList'
2627

2728
/**
2829
* A `ScalarResource` is Any kind of resource that is directly
2930
* represented, as opposed to being implicitly represented by a
3031
* function call.
3132
*
3233
*/
33-
export type ScalarResource = string | HTMLElement | Table
34+
export type ScalarResource = string | HTMLElement | Table | DescriptionList
3435
export interface ScalarContent<T = ScalarResource> {
3536
content: T
3637
}
@@ -57,7 +58,7 @@ export function isScalarContent<T extends MetadataBearing>(entity: ScalarLike<T>
5758
return (
5859
isReactProvider(entity) ||
5960
(content !== undefined &&
60-
(typeof content === 'string' || isTable(content) || isHTML(content)))
61+
(typeof content === 'string' || isTable(content) || isHTML(content) || isDescriptionList(content)))
6162
)
6263
}
6364

packages/test/src/api/selectors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,8 @@ export const EXPANDABLE_OUTPUT_N = (N: number, splitIndex = 1) =>
361361
`${PROMPT_BLOCK_N_FOR_SPLIT(N, splitIndex)} .kui--expandable-section button`
362362
export const EXPANDABLE_OUTPUT_LAST = (splitIndex = 1) =>
363363
`${PROMPT_BLOCK_LAST_FOR_SPLIT(splitIndex)} .kui--expandable-section button`
364+
365+
/** DescriptionList */
366+
export const DLIST = '.kui--description-list'
367+
export const DLIST_DESCRIPTION_FOR = (term: string) =>
368+
`.kui--description-list-term[data-term=${term}] + .kui--description-list-description`

packages/test/src/api/sidecar-expect.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,36 @@ export function breadcrumbs(breadcrumbs: string[]) {
379379
return res
380380
}
381381
}
382+
383+
/** Expect DescriptionList content in the current tab */
384+
export const descriptionList = (content: Record<string, number | boolean | string>) => async (res: AppAndCount) => {
385+
const container =
386+
res.splitIndex !== undefined
387+
? `${Selectors.PROMPT_BLOCK_N_FOR_SPLIT(res.count, res.splitIndex)} .kui--tab-content:not([hidden])`
388+
: `${Selectors.PROMPT_BLOCK_N(res.count)} .kui--tab-content:not([hidden])`
389+
390+
for (const term in content) {
391+
let idx = 0
392+
await res.app.client.waitUntil(
393+
async () => {
394+
const expectedDescription = content[term].toString() // the actualDescription will come as a string
395+
const actualDescription = await res.app.client
396+
.$(`${container} ${Selectors.DLIST_DESCRIPTION_FOR(term)}`)
397+
.then(_ => _.getText())
398+
if (actualDescription !== expectedDescription) {
399+
if (++idx > 5) {
400+
console.error(
401+
`Still waiting for expectedDescription=${expectedDescription} actualDescription=${actualDescription} `
402+
)
403+
}
404+
return false
405+
} else {
406+
return true
407+
}
408+
},
409+
{ timeout: waitTimeout }
410+
)
411+
}
412+
413+
return res
414+
}

plugins/plugin-client-common/src/components/Content/KuiContent.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
isFunctionContent,
3333
isScalarContent,
3434
isStringDiffContent,
35+
isDescriptionList,
3536
MultiModalResponse,
3637
ToolbarProps
3738
} from '@kui-shell/core'
@@ -45,6 +46,7 @@ import HTMLString from './HTMLString'
4546
import HTMLDom from './Scalar/HTMLDom'
4647
import { KuiContext } from '../../'
4748
import RadioTableSpi from '../spi/RadioTable'
49+
import DescriptionList from '../spi/DescriptionList'
4850

4951
export type KuiMMRProps = ToolbarProps & {
5052
tab: KuiTab
@@ -140,6 +142,14 @@ export default class KuiContent extends React.PureComponent<KuiMMRProps, State>
140142
} else if (isScalarContent(mode)) {
141143
if (isReactProvider(mode)) {
142144
return mode.react({ willUpdateToolbar })
145+
} else if (isDescriptionList(mode.content)) {
146+
return (
147+
<div className="flex-fill flex-layout flex-align-stretch">
148+
<div className="scrollable scrollable-auto scrollable-x flex-fill flex-layout flex-align-stretch">
149+
<DescriptionList groups={mode.content.spec.groups} className="left-pad right-pad" />
150+
</div>
151+
</div>
152+
)
143153
} else if (isRadioTable(mode.content)) {
144154
const radioTable = mode.content
145155
// ^^^ Notes: Even though isRadioTable(mode.content) checks the type of mode.content,

plugins/plugin-client-common/src/components/Views/Sidecar/TopNavSidecarV2.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export function getStateFromMMR(tab: KuiTab, response: MultiModalResponse): Hist
9797

9898
// toolbarText: if the default mode specified one, then use it;
9999
// otherwise, use the one specified by response
100-
const toolbarText = tabs[defaultMode] ? tabs[defaultMode].toolbarText : response.toolbarText
100+
const toolbarText = (tabs[defaultMode] && tabs[defaultMode].toolbarText) || response.toolbarText
101101

102102
return {
103103
currentTabIndex: defaultMode,

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,10 @@ export default class Input extends InputProvider {
710710
private experimentalTag() {
711711
if (this.props.isExperimental) {
712712
return (
713-
<span className="kui--repl-block-right-element left-pad kui--repl-block-experimental-tag-wrapper">
713+
<span
714+
className="kui--repl-block-right-element left-pad kui--repl-block-experimental-tag-wrapper"
715+
key="experimental-tag"
716+
>
714717
<Tag spanclassname="kui--repl-block-experimental-tag" title={strings('HoverExperimentalTag')} type="warning">
715718
{strings('ExperimentalTag')}
716719
</Tag>
@@ -755,7 +758,7 @@ export default class Input extends InputProvider {
755758
/** spinner for processing blocks */
756759
private spinner() {
757760
return (
758-
<span className="kui--repl-block-spinner">
761+
<span className="kui--repl-block-spinner" key="spinner">
759762
<Spinner />
760763
</span>
761764
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 React from 'react'
18+
import {
19+
DescriptionList,
20+
DescriptionListGroup,
21+
DescriptionListTerm,
22+
DescriptionListDescription
23+
} from '@patternfly/react-core'
24+
25+
import Props from '../model'
26+
27+
import '../../../../../web/scss/components/DescriptionList/PatternFly.scss'
28+
29+
function columnModifier(maxWidth: number) {
30+
if (maxWidth > 40) {
31+
return {
32+
default: '1Col' as const,
33+
lg: '2Col' as const,
34+
xl: '2Col' as const
35+
}
36+
} else {
37+
return {
38+
default: '1Col' as const,
39+
lg: '2Col' as const,
40+
xl: '3Col' as const
41+
}
42+
}
43+
}
44+
45+
export default function PatternFlyDescriptionList(props: Props) {
46+
const maxWidth = props.groups.reduce(
47+
(max, group) => Math.max(max, group.term.length /*, group.description.toString().length */),
48+
0
49+
)
50+
51+
return (
52+
<DescriptionList
53+
isAutoColumnWidths
54+
className={[props.className, 'kui--description-list', 'flex-fill'].join(' ')}
55+
columnModifier={columnModifier(maxWidth)}
56+
>
57+
{props.groups.map((group, idx) => (
58+
<DescriptionListGroup key={idx} className="kui--description-list-group">
59+
<DescriptionListTerm className="kui--description-list-term" data-term={group.term}>
60+
{group.term}
61+
</DescriptionListTerm>
62+
<DescriptionListDescription className="kui--description-list-description">
63+
{group.description}
64+
</DescriptionListDescription>
65+
</DescriptionListGroup>
66+
))}
67+
</DescriptionList>
68+
)
69+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 React from 'react'
18+
import Props from './model'
19+
20+
const PatternFly4 = React.lazy(() => import('./impl/PatternFly'))
21+
22+
export { Props }
23+
24+
export default function DescriptionListSpi(props: Props): React.ReactElement {
25+
return <PatternFly4 {...props} />
26+
}

0 commit comments

Comments
 (0)