Skip to content

Commit 5c65259

Browse files
authored
Merge pull request #4375 from Blargian/privatelink_clickpipes_improvement
Enhancement: support numbered lists for vertical stepper
2 parents 0c9e79b + a6de22c commit 5c65259

File tree

4 files changed

+174
-30
lines changed

4 files changed

+174
-30
lines changed

contribute/style-guide.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,48 @@ vale --filter='.Name == "ClickHouse.Headings"' docs/integrations
473473
This will run only the rule named `Headings` on
474474
the `docs/integrations` directory. Specifying a specific markdown
475475
file is also possible.
476+
477+
## Vertical numbered stepper
478+
479+
It is possible to render numbered steppers, as seen [here](https://clickhouse.com/docs/getting-started/quick-start/cloud)
480+
for example, using the following syntax:
481+
482+
`<VerticalStepper headerLevel="hN"></VerticalStepper>`
483+
484+
For example:
485+
486+
```markdown
487+
<VerticalStepper headerLevel="h2">
488+
## Header 1 {#explicit-anchor-1}
489+
490+
Some content...
491+
492+
## Header 2 {#explicit-anchor-2}
493+
494+
Some more content...
495+
496+
</VerticalStepper>
497+
```
498+
499+
You should specify `N` as the header level you want the vertical stepper to render
500+
for. In the example above, it is `h2` as we are using `##`. Use `h3` for `###`,
501+
`h4` for `####` etc.
502+
503+
The component also works with numbered lists using `headerLevel="list"`. For example:
504+
505+
```markdown
506+
<VerticalStepper headerLevel="h2">
507+
508+
1. First list item
509+
510+
Some content...
511+
512+
2. Second list item
513+
514+
Some more content...
515+
516+
</VerticalStepper>
517+
```
518+
519+
In this case, the first paragraph will be taken to be the label (the text next
520+
to the numbered circles of the vertical stepper) of the stepper.

docs/integrations/data-ingestion/clickpipes/aws-privatelink.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ To set up PrivateLink with VPC resource:
5454
2. Create a resource configuration
5555
3. Create a resource share
5656

57-
#### 1. Create a resource gateway {#create-resource-gateway}
57+
<VerticalStepper headerLevel="h4">
58+
59+
#### Create a resource gateway {#create-resource-gateway}
5860

5961
Resource gateway is the point that receives traffic for specified resources in your VPC.
6062

@@ -85,7 +87,7 @@ aws vpc-lattice get-resource-gateway \
8587
--resource-gateway-identifier <RESOURCE_GATEWAY_ID>
8688
```
8789

88-
#### 2. Create a VPC Resource-Configuration {#create-resource-configuration}
90+
#### Create a VPC Resource-Configuration {#create-resource-configuration}
8991

9092
Resource-Configuration is associated with resource gateway to make your resource accessible.
9193

@@ -121,7 +123,7 @@ For more information, see the [AWS documentation](https://docs.aws.amazon.com/vp
121123

122124
The output will contain a Resource-Configuration ARN, which you will need for the next step. It will also contain a Resource-Configuration ID, which you will need to set up a ClickPipe connection with VPC resource.
123125

124-
#### 3. Create a Resource-Share {#create-resource-share}
126+
#### Create a Resource-Share {#create-resource-share}
125127

126128
Sharing your resource requires a Resource-Share. This is facilitated through the Resource Access Manager (RAM).
127129

@@ -143,6 +145,8 @@ You are ready to [create a ClickPipe with Reverse private endpoint](#creating-cl
143145

144146
For more details on PrivateLink with VPC resource, see [AWS documentation](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-resources.html).
145147

148+
</VerticalStepper>
149+
146150
### MSK multi-VPC connectivity {#msk-multi-vpc}
147151

148152
The [Multi-VPC connectivity](https://docs.aws.amazon.com/msk/latest/developerguide/aws-access-mult-vpc.html) is a built-in feature of AWS MSK that allows you to connect multiple VPCs to a single MSK cluster.
@@ -188,6 +192,8 @@ can be configured for ClickPipes. Add [your ClickPipe region](#aws-privatelink-r
188192

189193
## Creating a ClickPipe with reverse private endpoint {#creating-clickpipe}
190194

195+
<VerticalStepper headerLevel="list">
196+
191197
1. Access the SQL Console for your ClickHouse Cloud Service.
192198

193199
<Image img={cp_service} alt="ClickPipes service" size="md" border/>
@@ -242,22 +248,28 @@ For same-region access, creating a VPC Resource is the recommended approach.
242248

243249
To see a full list of DNS names, access it in the cloud service settings.
244250

251+
</VerticalStepper>
252+
245253
## Managing existing reverse private endpoints {#managing-existing-endpoints}
246254

247255
You can manage existing reverse private endpoints in the ClickHouse Cloud service settings:
248256

257+
<VerticalStepper headerLevel="list">
258+
249259
1. On a sidebar find the `Settings` button and click on it.
250260

251-
<Image img={cp_rpe_settings0} alt="ClickHouse Cloud settings" size="lg" border/>
261+
<Image img={cp_rpe_settings0} alt="ClickHouse Cloud settings" size="lg" border/>
252262

253263
2. Click on `Reverse private endpoints` in a `ClickPipe reverse private endpoints` section.
254264

255-
<Image img={cp_rpe_settings1} alt="ClickHouse Cloud settings" size="md" border/>
265+
<Image img={cp_rpe_settings1} alt="ClickHouse Cloud settings" size="md" border/>
256266

257267
Reverse private endpoint extended information is shown in the flyout.
258268

259269
Endpoint can be removed from here. It will affect any ClickPipes using this endpoint.
260270

271+
</VerticalStepper>
272+
261273
## Supported AWS regions {#aws-privatelink-regions}
262274

263275
AWS PrivateLink support is limited to specific AWS regions for ClickPipes.

plugins/remark-custom-blocks.js

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ const extractText = (nodes) => {
1414
return text.trim();
1515
};
1616

17+
const extractRawContent = (nodes) => {
18+
if (!nodes || !Array.isArray(nodes)) return '';
19+
return nodes.map(node => {
20+
if (node.type === 'text') {
21+
return node.value;
22+
} else if (node.type === 'inlineCode') {
23+
return `\`${node.value}\``;
24+
} else if (node.children) {
25+
return extractRawContent(node.children);
26+
}
27+
return '';
28+
}).join('');
29+
};
30+
1731
// --- Main Plugin Function ---
1832
const plugin = (options) => {
1933
const transformer = (tree, file) => {
@@ -38,13 +52,17 @@ const plugin = (options) => {
3852
type = attr.value;
3953
} else if (attr.name === 'headerLevel' && typeof attr.value === 'string') {
4054
let set_level = attr.value
41-
const regex = /h([2-5])/;
42-
const match = set_level.match(regex);
43-
// If there's a match, convert the captured group to a number
44-
if (match) {
45-
headerLevel = Number(match[1]);
55+
if (set_level === 'list') {
56+
headerLevel = 'list';
4657
} else {
47-
throw new Error("VerticalStepper supported only for h2-5");
58+
const regex = /h([2-5])/;
59+
const match = set_level.match(regex);
60+
// If there's a match, convert the captured group to a number
61+
if (match) {
62+
headerLevel = Number(match[1]);
63+
} else {
64+
throw new Error("VerticalStepper supported only for h2-5 or 'list'");
65+
}
4866
}
4967
}
5068
}
@@ -72,18 +90,47 @@ const plugin = (options) => {
7290
};
7391

7492
if (node.children && node.children.length > 0) {
75-
node.children.forEach((child) => {
76-
if (child.type === 'heading' && child.depth === headerLevel) {
77-
finalizeStep(); // Finalize the previous step first
78-
currentStepLabel = extractText(child.children);
79-
currentAnchorId = child.data?.hProperties?.id || null;
80-
currentStepId = `step-${total_steps}`; // Generate step-X ID
81-
currentStepContent.push(child); // We need the header otherwise onBrokenAnchors fails
82-
} else if (currentStepLabel) {
83-
// Only collect content nodes *after* a heading has defined a step
84-
currentStepContent.push(child);
85-
}
86-
});
93+
if (headerLevel === 'list') {
94+
// Handle ordered list mode
95+
node.children.forEach((child) => {
96+
if (child.type === 'list' && child.ordered === true) {
97+
// Process each list item as a step
98+
child.children.forEach((listItem) => {
99+
if (listItem.type === 'listItem' && listItem.children && listItem.children.length > 0) {
100+
finalizeStep(); // Finalize the previous step first
101+
// Extract the first paragraph as the step label
102+
const firstChild = listItem.children[0];
103+
if (firstChild && firstChild.type === 'paragraph') {
104+
currentStepLabel = firstChild.children;
105+
currentStepId = `step-${total_steps}`;
106+
currentAnchorId = null;
107+
// Include all list item content except the first paragraph (which becomes the label)
108+
currentStepContent.push(...listItem.children.slice(1));
109+
}
110+
}
111+
});
112+
} else {
113+
// Include other content (like paragraphs, images, etc.) in the current step
114+
if (currentStepLabel) {
115+
currentStepContent.push(child);
116+
}
117+
}
118+
});
119+
} else {
120+
// Handle heading mode (original logic)
121+
node.children.forEach((child) => {
122+
if (child.type === 'heading' && child.depth === headerLevel) {
123+
finalizeStep(); // Finalize the previous step first
124+
currentStepLabel = extractText(child.children);
125+
currentAnchorId = child.data?.hProperties?.id || null;
126+
currentStepId = `step-${total_steps}`; // Generate step-X ID
127+
currentStepContent.push(child); // We need the header otherwise onBrokenAnchors fails
128+
} else if (currentStepLabel) {
129+
// Only collect content nodes *after* a heading has defined a step
130+
currentStepContent.push(child);
131+
}
132+
});
133+
}
87134
}
88135
finalizeStep(); // Finalize the last step found
89136

@@ -110,9 +157,31 @@ const plugin = (options) => {
110157
// Basic attributes for Step
111158
const stepAttributes = [
112159
{ type: 'mdxJsxAttribute', name: 'id', value: step.id }, // step-X
113-
{ type: 'mdxJsxAttribute', name: 'label', value: step.label }, // Plain text
114160
];
115161

162+
// Add the label - for list mode, we'll create a special label element
163+
if (headerLevel === 'list' && Array.isArray(step.label)) {
164+
// For list mode, create a paragraph element with the label content and add it to the step children
165+
const labelParagraph = {
166+
type: 'paragraph',
167+
children: [...step.label]
168+
};
169+
step.content.unshift(labelParagraph);
170+
171+
// Use plain text for the label attribute
172+
stepAttributes.push({
173+
type: 'mdxJsxAttribute',
174+
name: 'label',
175+
value: extractRawContent(step.label)
176+
});
177+
} else {
178+
stepAttributes.push({
179+
type: 'mdxJsxAttribute',
180+
name: 'label',
181+
value: step.label
182+
});
183+
}
184+
116185
// Add forceExpanded attribute if parent was expanded
117186
// (Matches React prop name used before anchor logic)
118187
if (isExpanded) {

src/components/Stepper/Stepper.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,32 @@ const Step = ({
2929
// Let underlying component handle expansion based on status='active'
3030
const collapsed = true;
3131

32-
// Swap out the Click-UI Stepper label for the H2 header
32+
// Swap out the Click-UI Stepper label for custom content
3333
React.useEffect(() => {
3434
try {
3535
const button = document.querySelectorAll(`button[id^=${id}]`)[0];
3636
const divChildren = Array.from(button.children).filter(el => el.tagName === 'DIV');
3737
const label = divChildren[1];
3838
const content = button.nextElementSibling;
39-
const header = content.querySelectorAll(headerType)[0]
40-
header.style.margin = '0';
41-
button.append(header)
39+
40+
if (headerType === 'list') {
41+
// For list mode, find the first paragraph (which contains the formatted label)
42+
const firstParagraph = content.querySelector('p');
43+
if (firstParagraph) {
44+
const labelElement = firstParagraph.cloneNode(true);
45+
(labelElement as HTMLElement).style.margin = '0';
46+
button.append(labelElement);
47+
firstParagraph.remove(); // Remove from content to avoid duplication
48+
}
49+
} else {
50+
// For heading mode, use the header element
51+
const header = content.querySelectorAll(headerType)[0]
52+
if (header) {
53+
(header as HTMLElement).style.margin = '0';
54+
button.append(header)
55+
}
56+
}
57+
4258
label.remove()
4359
} catch (e) {
4460
console.log(`Error occurred in Stepper.tsx while swapping ${headerType} for Click-UI label:`, e)
@@ -71,7 +87,7 @@ interface StepperProps {
7187
type?: 'numbered' | 'bulleted';
7288
className?: string;
7389
expanded?: string; // Corresponds to allExpanded in MDX
74-
headerLevel?: number;
90+
headerLevel?: number | string;
7591
[key: string]: any;
7692
}
7793

@@ -89,7 +105,9 @@ const VStepper = ({
89105
const isExpandedMode = expanded === 'true';
90106

91107
let hType = 'h2';
92-
if (headerLevel > 2) {
108+
if (headerLevel === 'list') {
109+
hType = 'list';
110+
} else if (headerLevel > 2) {
93111
hType = `h${headerLevel}`
94112
}
95113

0 commit comments

Comments
 (0)