From a6de22c7b075db03ed22dc197f345d533600f11f Mon Sep 17 00:00:00 2001
From: Shaun Struwig <41984034+Blargian@users.noreply.github.com>
Date: Tue, 2 Sep 2025 19:31:26 +0200
Subject: [PATCH] support numbered lists for vertical stepper, and update AWS
Private Link ClickPipes page to use it
---
contribute/style-guide.md | 45 ++++++++
.../clickpipes/aws-privatelink.md | 22 +++-
plugins/remark-custom-blocks.js | 107 ++++++++++++++----
src/components/Stepper/Stepper.tsx | 30 ++++-
4 files changed, 174 insertions(+), 30 deletions(-)
diff --git a/contribute/style-guide.md b/contribute/style-guide.md
index 60162654227..816e9bb3c72 100644
--- a/contribute/style-guide.md
+++ b/contribute/style-guide.md
@@ -473,3 +473,48 @@ vale --filter='.Name == "ClickHouse.Headings"' docs/integrations
This will run only the rule named `Headings` on
the `docs/integrations` directory. Specifying a specific markdown
file is also possible.
+
+## Vertical numbered stepper
+
+It is possible to render numbered steppers, as seen [here](https://clickhouse.com/docs/getting-started/quick-start/cloud)
+for example, using the following syntax:
+
+``
+
+For example:
+
+```markdown
+
+## Header 1 {#explicit-anchor-1}
+
+Some content...
+
+## Header 2 {#explicit-anchor-2}
+
+Some more content...
+
+
+```
+
+You should specify `N` as the header level you want the vertical stepper to render
+for. In the example above, it is `h2` as we are using `##`. Use `h3` for `###`,
+`h4` for `####` etc.
+
+The component also works with numbered lists using `headerLevel="list"`. For example:
+
+```markdown
+
+
+1. First list item
+
+Some content...
+
+2. Second list item
+
+Some more content...
+
+
+```
+
+In this case, the first paragraph will be taken to be the label (the text next
+to the numbered circles of the vertical stepper) of the stepper.
diff --git a/docs/integrations/data-ingestion/clickpipes/aws-privatelink.md b/docs/integrations/data-ingestion/clickpipes/aws-privatelink.md
index 73213243b79..11b92b4268d 100644
--- a/docs/integrations/data-ingestion/clickpipes/aws-privatelink.md
+++ b/docs/integrations/data-ingestion/clickpipes/aws-privatelink.md
@@ -54,7 +54,9 @@ To set up PrivateLink with VPC resource:
2. Create a resource configuration
3. Create a resource share
-#### 1. Create a resource gateway {#create-resource-gateway}
+
+
+#### Create a resource gateway {#create-resource-gateway}
Resource gateway is the point that receives traffic for specified resources in your VPC.
@@ -85,7 +87,7 @@ aws vpc-lattice get-resource-gateway \
--resource-gateway-identifier
```
-#### 2. Create a VPC Resource-Configuration {#create-resource-configuration}
+#### Create a VPC Resource-Configuration {#create-resource-configuration}
Resource-Configuration is associated with resource gateway to make your resource accessible.
@@ -121,7 +123,7 @@ For more information, see the [AWS documentation](https://docs.aws.amazon.com/vp
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.
-#### 3. Create a Resource-Share {#create-resource-share}
+#### Create a Resource-Share {#create-resource-share}
Sharing your resource requires a Resource-Share. This is facilitated through the Resource Access Manager (RAM).
@@ -143,6 +145,8 @@ You are ready to [create a ClickPipe with Reverse private endpoint](#creating-cl
For more details on PrivateLink with VPC resource, see [AWS documentation](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-resources.html).
+
+
### MSK multi-VPC connectivity {#msk-multi-vpc}
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
## Creating a ClickPipe with reverse private endpoint {#creating-clickpipe}
+
+
1. Access the SQL Console for your ClickHouse Cloud Service.
@@ -242,22 +248,28 @@ For same-region access, creating a VPC Resource is the recommended approach.
To see a full list of DNS names, access it in the cloud service settings.
+
+
## Managing existing reverse private endpoints {#managing-existing-endpoints}
You can manage existing reverse private endpoints in the ClickHouse Cloud service settings:
+
+
1. On a sidebar find the `Settings` button and click on it.
-
+
2. Click on `Reverse private endpoints` in a `ClickPipe reverse private endpoints` section.
-
+
Reverse private endpoint extended information is shown in the flyout.
Endpoint can be removed from here. It will affect any ClickPipes using this endpoint.
+
+
## Supported AWS regions {#aws-privatelink-regions}
AWS PrivateLink support is limited to specific AWS regions for ClickPipes.
diff --git a/plugins/remark-custom-blocks.js b/plugins/remark-custom-blocks.js
index 6c6895fd2df..1d045d6f468 100644
--- a/plugins/remark-custom-blocks.js
+++ b/plugins/remark-custom-blocks.js
@@ -14,6 +14,20 @@ const extractText = (nodes) => {
return text.trim();
};
+const extractRawContent = (nodes) => {
+ if (!nodes || !Array.isArray(nodes)) return '';
+ return nodes.map(node => {
+ if (node.type === 'text') {
+ return node.value;
+ } else if (node.type === 'inlineCode') {
+ return `\`${node.value}\``;
+ } else if (node.children) {
+ return extractRawContent(node.children);
+ }
+ return '';
+ }).join('');
+};
+
// --- Main Plugin Function ---
const plugin = (options) => {
const transformer = (tree, file) => {
@@ -38,13 +52,17 @@ const plugin = (options) => {
type = attr.value;
} else if (attr.name === 'headerLevel' && typeof attr.value === 'string') {
let set_level = attr.value
- const regex = /h([2-5])/;
- const match = set_level.match(regex);
- // If there's a match, convert the captured group to a number
- if (match) {
- headerLevel = Number(match[1]);
+ if (set_level === 'list') {
+ headerLevel = 'list';
} else {
- throw new Error("VerticalStepper supported only for h2-5");
+ const regex = /h([2-5])/;
+ const match = set_level.match(regex);
+ // If there's a match, convert the captured group to a number
+ if (match) {
+ headerLevel = Number(match[1]);
+ } else {
+ throw new Error("VerticalStepper supported only for h2-5 or 'list'");
+ }
}
}
}
@@ -72,18 +90,47 @@ const plugin = (options) => {
};
if (node.children && node.children.length > 0) {
- node.children.forEach((child) => {
- if (child.type === 'heading' && child.depth === headerLevel) {
- finalizeStep(); // Finalize the previous step first
- currentStepLabel = extractText(child.children);
- currentAnchorId = child.data?.hProperties?.id || null;
- currentStepId = `step-${total_steps}`; // Generate step-X ID
- currentStepContent.push(child); // We need the header otherwise onBrokenAnchors fails
- } else if (currentStepLabel) {
- // Only collect content nodes *after* a heading has defined a step
- currentStepContent.push(child);
- }
- });
+ if (headerLevel === 'list') {
+ // Handle ordered list mode
+ node.children.forEach((child) => {
+ if (child.type === 'list' && child.ordered === true) {
+ // Process each list item as a step
+ child.children.forEach((listItem) => {
+ if (listItem.type === 'listItem' && listItem.children && listItem.children.length > 0) {
+ finalizeStep(); // Finalize the previous step first
+ // Extract the first paragraph as the step label
+ const firstChild = listItem.children[0];
+ if (firstChild && firstChild.type === 'paragraph') {
+ currentStepLabel = firstChild.children;
+ currentStepId = `step-${total_steps}`;
+ currentAnchorId = null;
+ // Include all list item content except the first paragraph (which becomes the label)
+ currentStepContent.push(...listItem.children.slice(1));
+ }
+ }
+ });
+ } else {
+ // Include other content (like paragraphs, images, etc.) in the current step
+ if (currentStepLabel) {
+ currentStepContent.push(child);
+ }
+ }
+ });
+ } else {
+ // Handle heading mode (original logic)
+ node.children.forEach((child) => {
+ if (child.type === 'heading' && child.depth === headerLevel) {
+ finalizeStep(); // Finalize the previous step first
+ currentStepLabel = extractText(child.children);
+ currentAnchorId = child.data?.hProperties?.id || null;
+ currentStepId = `step-${total_steps}`; // Generate step-X ID
+ currentStepContent.push(child); // We need the header otherwise onBrokenAnchors fails
+ } else if (currentStepLabel) {
+ // Only collect content nodes *after* a heading has defined a step
+ currentStepContent.push(child);
+ }
+ });
+ }
}
finalizeStep(); // Finalize the last step found
@@ -110,9 +157,31 @@ const plugin = (options) => {
// Basic attributes for Step
const stepAttributes = [
{ type: 'mdxJsxAttribute', name: 'id', value: step.id }, // step-X
- { type: 'mdxJsxAttribute', name: 'label', value: step.label }, // Plain text
];
+ // Add the label - for list mode, we'll create a special label element
+ if (headerLevel === 'list' && Array.isArray(step.label)) {
+ // For list mode, create a paragraph element with the label content and add it to the step children
+ const labelParagraph = {
+ type: 'paragraph',
+ children: [...step.label]
+ };
+ step.content.unshift(labelParagraph);
+
+ // Use plain text for the label attribute
+ stepAttributes.push({
+ type: 'mdxJsxAttribute',
+ name: 'label',
+ value: extractRawContent(step.label)
+ });
+ } else {
+ stepAttributes.push({
+ type: 'mdxJsxAttribute',
+ name: 'label',
+ value: step.label
+ });
+ }
+
// Add forceExpanded attribute if parent was expanded
// (Matches React prop name used before anchor logic)
if (isExpanded) {
diff --git a/src/components/Stepper/Stepper.tsx b/src/components/Stepper/Stepper.tsx
index bb69b12078a..18432a7faae 100644
--- a/src/components/Stepper/Stepper.tsx
+++ b/src/components/Stepper/Stepper.tsx
@@ -29,16 +29,32 @@ const Step = ({
// Let underlying component handle expansion based on status='active'
const collapsed = true;
- // Swap out the Click-UI Stepper label for the H2 header
+ // Swap out the Click-UI Stepper label for custom content
React.useEffect(() => {
try {
const button = document.querySelectorAll(`button[id^=${id}]`)[0];
const divChildren = Array.from(button.children).filter(el => el.tagName === 'DIV');
const label = divChildren[1];
const content = button.nextElementSibling;
- const header = content.querySelectorAll(headerType)[0]
- header.style.margin = '0';
- button.append(header)
+
+ if (headerType === 'list') {
+ // For list mode, find the first paragraph (which contains the formatted label)
+ const firstParagraph = content.querySelector('p');
+ if (firstParagraph) {
+ const labelElement = firstParagraph.cloneNode(true);
+ (labelElement as HTMLElement).style.margin = '0';
+ button.append(labelElement);
+ firstParagraph.remove(); // Remove from content to avoid duplication
+ }
+ } else {
+ // For heading mode, use the header element
+ const header = content.querySelectorAll(headerType)[0]
+ if (header) {
+ (header as HTMLElement).style.margin = '0';
+ button.append(header)
+ }
+ }
+
label.remove()
} catch (e) {
console.log(`Error occurred in Stepper.tsx while swapping ${headerType} for Click-UI label:`, e)
@@ -71,7 +87,7 @@ interface StepperProps {
type?: 'numbered' | 'bulleted';
className?: string;
expanded?: string; // Corresponds to allExpanded in MDX
- headerLevel?: number;
+ headerLevel?: number | string;
[key: string]: any;
}
@@ -89,7 +105,9 @@ const VStepper = ({
const isExpandedMode = expanded === 'true';
let hType = 'h2';
- if (headerLevel > 2) {
+ if (headerLevel === 'list') {
+ hType = 'list';
+ } else if (headerLevel > 2) {
hType = `h${headerLevel}`
}