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

Commit adc8beb

Browse files
committed
feat(plugins/plugin-client-common): initial support for code block ordering
1 parent 29393ce commit adc8beb

File tree

20 files changed

+666
-122
lines changed

20 files changed

+666
-122
lines changed

packages/test/src/api/Wizard.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default class Wizard {
2424
private readonly _navItemProgressStepper = '.kui--progress-stepper'
2525
private readonly _navItemProgressStep = '.kui--progress-step'
2626
private readonly _body = '.pf-c-wizard__main-body'
27+
private readonly _wizardProgressMeasure = '.pf-c-progress__measure'
2728

2829
public withTitle(title: string) {
2930
return `${this.wizard} ${this.title}[aria-label="${title}"]`
@@ -60,4 +61,8 @@ export default class Wizard {
6061
public navItemProgressStep(navIdx: number, stepIdx: number) {
6162
return `${this.navItemProgressSteps(navIdx)}:nth-child(${stepIdx + 1})`
6263
}
64+
65+
public get progressMeasure() {
66+
return `${this.wizard} ${this._wizardProgressMeasure}`
67+
}
6368
}

plugins/plugin-client-common/i18n/code_en_US.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"Code Block": "Code Block",
3+
"Optional Code Block": "Code Block *(this code block is optional)*",
34

45
"xOfy": "{0} of {1}",
6+
"xOfyFailingz": "{0} of {2} ({1} failure)",
7+
"xOfyFailingsz": "{0} of {2} ({1} failures)",
8+
59
"Completed tasks": "Completed tasks",
610

711
"status.blank": "Not yet executed",

plugins/plugin-client-common/src/components/Content/Markdown/components/Wizard/CodeBlockProps.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,26 @@ export default interface CodeBlockProps {
1818
id: string
1919
body: string
2020
language: string
21+
optional?: boolean
2122
validate?: string
23+
24+
/**
25+
* Is this a member of a group of choices? e.g. am I `A` in a choice
26+
* to do either `A+B` or `C+D`?
27+
*/
28+
choice?: {
29+
/**
30+
* This option names the group, to keep it distinct from other
31+
* groups of choices.
32+
*/
33+
group?: string
34+
35+
/**
36+
* This option names that member. e.g. if the user can choose
37+
* between doing either A-and-B or C-and-D, this identifies
38+
* whether we are part ofth e first choice (A+B) or the second
39+
* (C+D).
40+
*/
41+
member?: string
42+
}
2243
}

plugins/plugin-client-common/src/components/Content/Markdown/components/Wizard/Progress.tsx

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,22 @@
1717
import React from 'react'
1818
import { i18n } from '@kui-shell/core'
1919

20-
import CodeBlockProps from './CodeBlockProps'
20+
import { Graph, blocks, progress } from '../code/graph'
2121

2222
import { ProgressStepState, statusFromStatusVector } from '../../../ProgressStepper'
2323
import { subscribeToLinkUpdates, unsubscribeToLinkUpdates } from '../../../LinkStatus'
2424

25+
import { State as WizardState } from '.'
26+
2527
import { ProgressVariant } from '@patternfly/react-core'
2628

2729
const PatternFlyProgress = React.lazy(() => import('@patternfly/react-core').then(_ => ({ default: _.Progress })))
2830

2931
const strings = i18n('plugin-client-common', 'code')
3032

31-
interface Props {
33+
type Props = Pick<WizardState, 'choices'> & {
3234
/** The tasks to be accomplished */
33-
codeBlocks: CodeBlockProps[]
35+
codeBlocks: Graph
3436
}
3537

3638
type Status = ProgressStepState['status']
@@ -65,52 +67,35 @@ export default class Progress extends React.PureComponent<Props, State> {
6567
}
6668

6769
public componentDidMount() {
68-
this.props.codeBlocks.forEach(_ => {
70+
blocks(this.props.codeBlocks, 'all').forEach(_ => {
6971
subscribeToLinkUpdates(_.id, this._statusUpdateHandler)
7072
})
7173
}
7274

7375
public componentWillUnmount() {
74-
this.props.codeBlocks.forEach(_ => {
76+
blocks(this.props.codeBlocks, 'all').forEach(_ => {
7577
unsubscribeToLinkUpdates(_.id, this._statusUpdateHandler)
7678
})
7779
}
7880

7981
private get nSteps() {
80-
return this.props.codeBlocks.length
82+
return progress(this.props.codeBlocks, undefined, this.props.choices).nTotal
8183
}
8284

8385
private counts() {
84-
return Object.values(this.state.status).reduce(
85-
(counts, status) => {
86-
if (status === 'success') {
87-
counts.nDone++
88-
} else if (status === 'error') {
89-
counts.nError++
90-
} else if (status === 'in-progress') {
91-
counts.nInProgress++
92-
}
93-
94-
return counts
95-
},
96-
{ nDone: 0, nError: 0, nInProgress: 0 }
97-
)
86+
return progress(this.props.codeBlocks, this.state.status, this.props.choices)
9887
}
9988

10089
public render() {
101-
const { nDone, nError /* , nInProgress */ } = this.counts()
90+
const { nDone, nError } = this.counts()
10291

10392
const title = strings('Completed tasks')
104-
const label = /* nInProgress > 0 ? strings('status.in-progress') : */ strings('xOfy', nDone, this.nSteps)
105-
106-
const variant =
107-
nDone === this.nSteps
108-
? ProgressVariant.success
109-
: nError > 0
110-
? ProgressVariant.danger
111-
: // : nInProgress > 0
112-
// ? ProgressVariant.warning
113-
undefined
93+
const label =
94+
nError > 0
95+
? strings(nError === 1 ? 'xOfyFailingz' : 'xOfyFailingsz', nDone, nError, this.nSteps)
96+
: strings('xOfy', nDone, this.nSteps)
97+
98+
const variant = nDone === this.nSteps ? ProgressVariant.success : nError > 0 ? ProgressVariant.danger : undefined
11499

115100
return (
116101
<PatternFlyProgress

plugins/plugin-client-common/src/components/Content/Markdown/components/Wizard/index.tsx

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,66 @@ import { WizardProps } from './rehype-wizard'
2020

2121
import Progress from './Progress'
2222
import CodeBlockProps from './CodeBlockProps'
23+
import { onTabSwitch, offTabSwitch } from '../tabbed'
24+
import { Graph, sequence, compile, blocks } from '../code/graph'
2325

2426
import Card from '../../../../spi/Card'
25-
import { MiniProgressStepper, MiniProgressStep } from '../../../MiniProgressStepper'
27+
import { MiniProgressStepper } from '../../../MiniProgressStepper'
2628

2729
import '../../../../../../web/scss/components/Wizard/PatternFly.scss'
2830

2931
const PatternFlyWizard = React.lazy(() => import('@patternfly/react-core').then(_ => ({ default: _.Wizard })))
3032

31-
export default class Wizard extends React.PureComponent<WizardProps> {
32-
private wizardCodeBlockSteps(containedCodeBlocks: CodeBlockProps[]) {
33+
type Props = WizardProps & { uuid: string }
34+
35+
export interface State {
36+
/** Map from tab group to currently selected tab member */
37+
choices: Record<string, string>
38+
}
39+
40+
export default class Wizard extends React.PureComponent<Props, State> {
41+
private readonly cleaners: (() => void)[] = []
42+
43+
public constructor(props: Props) {
44+
super(props)
45+
46+
this.state = {
47+
choices: {}
48+
}
49+
}
50+
51+
public componentDidMount() {
52+
const switcher = (group: string, member: string) => {
53+
this.setState(curState => ({
54+
choices: Object.assign({}, curState.choices, { [group]: member })
55+
}))
56+
}
57+
58+
onTabSwitch(this.props.uuid, switcher)
59+
this.cleaners.push(() => offTabSwitch(this.props.uuid, switcher))
60+
}
61+
62+
public componentDidUnmount() {
63+
this.cleaners.forEach(_ => _())
64+
}
65+
66+
private wizardCodeBlockSteps(containedCodeBlocks: Graph) {
3367
return (
34-
containedCodeBlocks.length > 0 && (
35-
<MiniProgressStepper>
36-
{containedCodeBlocks.map((_, idx) => (
37-
<MiniProgressStep key={idx} codeBlockId={_.id} validate={_.validate} body={_.body} language={_.language} />
38-
))}
39-
</MiniProgressStepper>
68+
containedCodeBlocks && (
69+
<MiniProgressStepper
70+
steps={blocks(containedCodeBlocks, this.state.choices).map(_ => ({
71+
codeBlockId: _.id,
72+
validate: _.validate,
73+
body: _.body,
74+
language: _.language,
75+
optional: _.optional
76+
}))}
77+
/>
4078
)
4179
)
4280
}
4381

44-
private wizardStepDescription(description: string, containedCodeBlocks: CodeBlockProps[]) {
82+
private wizardStepDescription(description: string, containedCodeBlocks: Graph) {
4583
return (
4684
<div className="kui--wizard-nav-item-description">
4785
{this.wizardCodeBlockSteps(containedCodeBlocks)}
@@ -50,14 +88,16 @@ export default class Wizard extends React.PureComponent<WizardProps> {
5088
)
5189
}
5290

53-
private containedCodeBlocks(_: WizardProps['children'][0]): CodeBlockProps[] {
91+
private containedCodeBlocks(_: WizardProps['children'][0]): Graph {
5492
if (typeof _.props.containedCodeBlocks === 'string' && _.props.containedCodeBlocks.length > 0) {
55-
return _.props.containedCodeBlocks
56-
.split(' ')
57-
.filter(Boolean)
58-
.map(_ => JSON.parse(Buffer.from(_, 'base64').toString()) as CodeBlockProps)
93+
return compile(
94+
_.props.containedCodeBlocks
95+
.split(' ')
96+
.filter(Boolean)
97+
.map(_ => JSON.parse(Buffer.from(_, 'base64').toString()) as CodeBlockProps)
98+
)
5999
} else {
60-
return []
100+
return undefined
61101
}
62102
}
63103

@@ -72,11 +112,11 @@ export default class Wizard extends React.PureComponent<WizardProps> {
72112
/** Overall progress across all steps */
73113
private progress() {
74114
if (this.props['data-kui-wizard-progress'] === 'bar') {
75-
const codeBlocks = this.children.flatMap(_ => this.containedCodeBlocks(_))
115+
const allCodeBlocks = sequence(this.children.flatMap(_ => this.containedCodeBlocks(_)))
76116
return (
77-
codeBlocks.length > 0 && (
117+
allCodeBlocks.sequence.length > 0 && (
78118
<div className="kui--markdown-major-paragraph">
79-
<Progress codeBlocks={codeBlocks} />
119+
<Progress choices={this.state.choices} codeBlocks={allCodeBlocks} />
80120
</div>
81121
)
82122
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Markdown Code Block Handling
2+
3+
## Execution Ordering Constraints
4+
5+
- markdown tabs represent an exclusive set of options
6+
- code blocks within a tab must be executed sequentially
7+
- code blocks within a wizard step can be executed in any order
8+
- across wizard steps, the groups of code blocks in each step must be executed sequentially in the order of the wizard steps
9+
10+
```
11+
*************
12+
Wizard Step 1
13+
CodeA
14+
*************
15+
16+
*************
17+
Wizard Step 2
18+
19+
|Tab1 | | Tab 2 |
20+
CodeB CodeC
21+
CodeD
22+
CodeE
23+
*************
24+
25+
*************
26+
Wizard Step 3
27+
CodeF
28+
*************
29+
```
30+
31+
Under those rules, a partial ordering of this markdown is:
32+
33+
CodeA -> CodeB -> CodeF
34+
\-> CodeC -> CodeD -> CodeE -/
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2022 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+
/** Re-serialize the given attributes and code block body */
18+
export default function dump(attributes: Record<string, any>, body: string) {
19+
return `
20+
---
21+
${require('js-yaml').dump(attributes)}
22+
---
23+
${body}
24+
`
25+
}

0 commit comments

Comments
 (0)