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. ClickPipes 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. -ClickHouse Cloud settings + ClickHouse Cloud settings 2. Click on `Reverse private endpoints` in a `ClickPipe reverse private endpoints` section. -ClickHouse Cloud settings + ClickHouse Cloud settings 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}` }