From 90be6204c5cc3f599a86d4a7e93d12f1f50232b6 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sat, 25 Apr 2026 17:00:37 +0530 Subject: [PATCH 01/11] Added support for Admitions and code copy button in serviceDoc panel --- .../BlockEditor/BlockEditor.interface.ts | 11 +++ .../BlockEditor/Extensions/AdmonitionNode.ts | 48 ++++++++++ .../CodeBlock/CodeBlockComponent.tsx | 57 +++++++++++ .../Extensions/CodeBlock/CodeBlockWithCopy.ts | 23 +++++ .../ServiceDocPanel/ServiceDocPanel.tsx | 2 +- .../ServiceDocPanel/service-doc-panel.less | 96 ++++++++++++++++++- .../utils/BlockEditorExtensionsClassBase.ts | 13 ++- .../resources/ui/src/utils/ServiceUtils.tsx | 42 ++++++-- 8 files changed, 282 insertions(+), 10 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockWithCopy.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.interface.ts index e57e3cb56609..c1209e15be8d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BlockEditor.interface.ts @@ -58,6 +58,17 @@ export interface ExtensionOptions { * @default false */ enableSectionNode?: boolean; + /** + * Enable admonition node extension to render
blocks. + * Required when rendering connector documentation with $$note/$$warning/etc. blocks. + * @default false + */ + enableAdmonitionNode?: boolean; + /** + * Replace the default code block with one that includes a copy-to-clipboard button. + * @default false + */ + enableCodeBlockCopy?: boolean; } export interface BlockEditorProps { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts new file mode 100644 index 000000000000..7f2380d44b28 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mergeAttributes, Node } from '@tiptap/core'; + +const AdmonitionNode = Node.create({ + name: 'admonition', + + group: 'block', + content: 'block+', + + addAttributes() { + return { + type: { + default: 'note', + parseHTML: (element) => element.getAttribute('data-admonition') ?? 'note', + renderHTML: (attributes) => ({ + 'data-admonition': attributes.type, + }), + }, + }; + }, + + parseHTML() { + return [{ tag: 'div[data-admonition]' }]; + }, + + renderHTML({ HTMLAttributes, node }) { + return [ + 'div', + mergeAttributes(HTMLAttributes, { + class: `admonition admonition-${node.attrs.type}`, + }), + 0, + ]; + }, +}); + +export default AdmonitionNode; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx new file mode 100644 index 000000000000..fe44a4a44538 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react'; +import { FC, useCallback, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import CopyIcon from '../../../../assets/svg/icon-copy.svg'; + +const CodeBlockComponent: FC = ({ node }) => { + const { t } = useTranslation(); + const [copied, setCopied] = useState(false); + const contentRef = useRef(null); + + const handleCopy = useCallback(async () => { + const text = contentRef.current?.textContent ?? node.textContent; + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // clipboard write failed silently + } + }, [node]); + + return ( + + + + {t('label.copied')} + + copy + + ); +}; + +export default CodeBlockComponent; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockWithCopy.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockWithCopy.ts new file mode 100644 index 000000000000..393dcaf10ca6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockWithCopy.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import CodeBlock from '@tiptap/extension-code-block'; +import { ReactNodeViewRenderer } from '@tiptap/react'; +import CodeBlockComponent from './CodeBlockComponent'; + +const CodeBlockWithCopy = CodeBlock.extend({ + addNodeView() { + return ReactNodeViewRenderer(CodeBlockComponent); + }, +}); + +export default CodeBlockWithCopy; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx index d0584a21c782..5d616c0a562f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx @@ -186,7 +186,7 @@ const ServiceDocPanel: FC = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less index 42101e1bc69e..0260c4bc286a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less @@ -98,18 +98,56 @@ } pre { + position: relative; margin: 8px 0; padding: 0; background: transparent; code { display: block; - padding: 15px; + padding: 15px 40px 15px 15px; overflow: auto; border-radius: 8px; border-bottom: 2px solid @border-color; background: @markdown-bg-color; } + + .code-copy-button { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + + &[data-copied='true'] { + opacity: 0.4; + } + } + + .code-copy-message { + position: absolute; + top: 10px; + right: 38px; + font-size: 12px; + color: @text-color; + background: @markdown-bg-color; + padding: 2px 6px; + border-radius: 4px; + display: none; + + &[data-copied='true'] { + display: inline; + } + } + + &:hover .code-copy-button { + opacity: 0.6; + + &:hover { + opacity: 1; + } + } } blockquote { @@ -146,6 +184,62 @@ margin-top: 12px; transition: ease-in-out; } + + .admonition { + border-radius: 8px; + border-left: 8px solid #afafc1; + padding: 16px 16px 16px 48px !important; + background-color: @markdown-bg-color; + position: relative; + margin: 10px 24px; + margin-bottom: 0; + + &::before { + position: absolute; + left: 12px; + font-size: 18px; + content: 'â„šī¸'; + } + + &.admonition-note { + &::before { + content: '📝'; + } + } + + &.admonition-warning, + &.admonition-caution { + border-left-color: @warning-color; + background: #fff3dc; + + &::before { + content: 'âš ī¸'; + } + } + + &.admonition-danger { + border-left-color: @error-color; + background: #ff4c3b1a; + + &::before { + content: '🚨'; + } + } + + &.admonition-tip { + border-left-color: @success-color; + background: #f6ffed; + + &::before { + content: '💡'; + } + } + + &.admonition-info { + border-left-color: @info-color; + background: @primary-1; + } + } } // Styling for EntitySummaryPanel when embedded in documentation diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts index 20bbd9a7794d..7d8bf8bd6899 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts @@ -38,6 +38,8 @@ import { LinkExtension } from '../components/BlockEditor/Extensions/link'; import MathEquation from '../components/BlockEditor/Extensions/MathEquation/MathEquation'; import { Mention } from '../components/BlockEditor/Extensions/mention'; import { mentionSuggestion } from '../components/BlockEditor/Extensions/mention/mentionSuggestions'; +import AdmonitionNode from '../components/BlockEditor/Extensions/AdmonitionNode'; +import CodeBlockWithCopy from '../components/BlockEditor/Extensions/CodeBlock/CodeBlockWithCopy'; import SectionNode from '../components/BlockEditor/Extensions/SectionNode'; import slashCommand from '../components/BlockEditor/Extensions/slash-command'; import { getSuggestionItems } from '../components/BlockEditor/Extensions/slash-command/items'; @@ -56,9 +58,10 @@ export class BlockEditorExtensionsClassBase { * Get the core extensions for the BlockEditor * These are the base extensions that are always included */ - protected getCoreExtensions(): Extensions { + protected getCoreExtensions(disableCodeBlock = false): Extensions { return [ StarterKit.configure({ + codeBlock: disableCodeBlock ? false : undefined, heading: { levels: [1, 2, 3], }, @@ -224,18 +227,24 @@ export class BlockEditorExtensionsClassBase { tableExtensions = true, advancedContextExtensions = true, enableSectionNode = false, + enableAdmonitionNode = false, + enableCodeBlockCopy = false, } = options ?? {}; return [ Document, Paragraph, Text, - ...(coreExtensions ? this.getCoreExtensions() : []), + ...(coreExtensions + ? this.getCoreExtensions(enableCodeBlockCopy) + : []), ...(enableHandlebars ? this.getHandlebarsExtensions() : []), ...(utilityExtensions ? this.getUtilityExtensions() : []), ...(tableExtensions ? this.getTableExtensions() : []), ...(advancedContextExtensions ? this.getAdvancedContentExtensions() : []), ...(enableSectionNode ? [SectionNode] : []), + ...(enableAdmonitionNode ? [AdmonitionNode] : []), + ...(enableCodeBlockCopy ? [CodeBlockWithCopy] : []), ]; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx index c15fb55a0895..7e6b854aaa52 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -678,21 +678,51 @@ export const validateServiceName = async ( return null; }; +const ADMONITION_TYPES = new Set([ + 'note', + 'warning', + 'danger', + 'info', + 'tip', + 'caution', +]); + +const ADMONITION_BLOCK_REGEX = + /^\$\$(note|warning|danger|info|tip|caution)\n([\s\S]*?)\n\$\$/gm; + +const convertAdmonitionsToHtml = (markdown: string): string => { + ADMONITION_BLOCK_REGEX.lastIndex = 0; + + return markdown.replace( + ADMONITION_BLOCK_REGEX, + (_match, type: string, content: string) => { + const admonitionType = ADMONITION_TYPES.has(type) ? type : 'note'; + const innerHtml = MarkdownToHTMLConverter.makeHtml(content.trim()); + + return `
${innerHtml}
`; + } + ); +}; + /** - * Converts markdown that uses $$section blocks into sanitizable HTML with - *
wrappers. Used by both ServiceDocPanel and SSODocPanel. + * Converts markdown with $$section and $$note/warning/etc. admonition blocks into + * sanitizable HTML. Used by both ServiceDocPanel and SSODocPanel. */ export const processDocMarkdown = (markdown: string): string => { + const withAdmonitions = convertAdmonitionsToHtml(markdown); + const parts: string[] = []; let lastIndex = 0; let match: RegExpExecArray | null; SECTION_BLOCK_REGEX.lastIndex = 0; - while ((match = SECTION_BLOCK_REGEX.exec(markdown)) !== null) { + while ((match = SECTION_BLOCK_REGEX.exec(withAdmonitions)) !== null) { if (match.index > lastIndex) { parts.push( - MarkdownToHTMLConverter.makeHtml(markdown.slice(lastIndex, match.index)) + MarkdownToHTMLConverter.makeHtml( + withAdmonitions.slice(lastIndex, match.index) + ) ); } @@ -710,8 +740,8 @@ export const processDocMarkdown = (markdown: string): string => { lastIndex = match.index + match[0].length; } - if (lastIndex < markdown.length) { - parts.push(MarkdownToHTMLConverter.makeHtml(markdown.slice(lastIndex))); + if (lastIndex < withAdmonitions.length) { + parts.push(MarkdownToHTMLConverter.makeHtml(withAdmonitions.slice(lastIndex))); } return parts.join('\n'); From 08a1878488c501dfbe546231f7314a20aa8db1fa Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sat, 25 Apr 2026 22:36:53 +0530 Subject: [PATCH 02/11] Addressed gitar comment --- .../CodeBlock/CodeBlockComponent.tsx | 31 +++++++++++-------- .../ServiceDocPanel/service-doc-panel.less | 4 +++ .../resources/ui/src/utils/ServiceUtils.tsx | 25 +++++---------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx index fe44a4a44538..a04e4de4c5e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx @@ -11,21 +11,28 @@ * limitations under the License. */ import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react'; -import { FC, useCallback, useRef, useState } from 'react'; +import { FC, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import CopyIcon from '../../../../assets/svg/icon-copy.svg'; const CodeBlockComponent: FC = ({ node }) => { const { t } = useTranslation(); const [copied, setCopied] = useState(false); - const contentRef = useRef(null); + const timerRef = useRef>(); + + useEffect(() => { + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, []); const handleCopy = useCallback(async () => { - const text = contentRef.current?.textContent ?? node.textContent; try { - await navigator.clipboard.writeText(text); + await navigator.clipboard.writeText(node.textContent); setCopied(true); - setTimeout(() => setCopied(false), 2000); + timerRef.current = setTimeout(() => setCopied(false), 2000); } catch { // clipboard write failed silently } @@ -33,23 +40,21 @@ const CodeBlockComponent: FC = ({ node }) => { return ( - + {t('label.copied')} - copy + type="button" + onClick={handleCopy}> + copy + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less index 0260c4bc286a..8cfbe1a34c92 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less @@ -119,6 +119,10 @@ cursor: pointer; opacity: 0; transition: opacity 0.2s; + background: none; + border: none; + padding: 0; + line-height: 0; &[data-copied='true'] { opacity: 0.4; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx index 7e6b854aaa52..88ed2886a0e3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -678,29 +678,20 @@ export const validateServiceName = async ( return null; }; -const ADMONITION_TYPES = new Set([ - 'note', - 'warning', - 'danger', - 'info', - 'tip', - 'caution', -]); - -const ADMONITION_BLOCK_REGEX = - /^\$\$(note|warning|danger|info|tip|caution)\n([\s\S]*?)\n\$\$/gm; +const ADMONITION_TYPES = ['note', 'warning', 'danger', 'info', 'tip', 'caution'] as const; + +const ADMONITION_BLOCK_REGEX = new RegExp( + `^\\$\\$(${ADMONITION_TYPES.join('|')})\\n([\\s\\S]*?)\\n\\$\\$`, + 'gm' +); const convertAdmonitionsToHtml = (markdown: string): string => { ADMONITION_BLOCK_REGEX.lastIndex = 0; return markdown.replace( ADMONITION_BLOCK_REGEX, - (_match, type: string, content: string) => { - const admonitionType = ADMONITION_TYPES.has(type) ? type : 'note'; - const innerHtml = MarkdownToHTMLConverter.makeHtml(content.trim()); - - return `
${innerHtml}
`; - } + (_match, type: string, content: string) => + `
${content.trim()}
` ); }; From 164ecdb038c0003cd345761bdebf53830011e882 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sat, 25 Apr 2026 22:46:07 +0530 Subject: [PATCH 03/11] lint fixes --- .../BlockEditor/Extensions/AdmonitionNode.ts | 3 ++- .../common/ServiceDocPanel/ServiceDocPanel.tsx | 6 +++++- .../resources/ui/src/constants/regex.constants.ts | 14 ++++++++++++++ .../ui/src/utils/BlockEditorExtensionsClassBase.ts | 8 +++----- .../main/resources/ui/src/utils/ServiceUtils.tsx | 11 ++++------- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts index 7f2380d44b28..6a1ce64a3b59 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts @@ -22,7 +22,8 @@ const AdmonitionNode = Node.create({ return { type: { default: 'note', - parseHTML: (element) => element.getAttribute('data-admonition') ?? 'note', + parseHTML: (element) => + element.getAttribute('data-admonition') ?? 'note', renderHTML: (attributes) => ({ 'data-admonition': attributes.type, }), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx index 5d616c0a562f..792ea69fa814 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.tsx @@ -186,7 +186,11 @@ const ServiceDocPanel: FC = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts index 52c4069e2215..836def549917 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts @@ -75,3 +75,17 @@ export const IMAGE_URL_PATTERN = /^(https?:\/\/.+|\/[^\s]+|data:image\/.+)|^[\w\-.]+\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i; export const SECTION_BLOCK_REGEX = /\$\$section\n([\s\S]*?)\n\$\$/g; + +const ADMONITION_TYPES = [ + 'note', + 'warning', + 'danger', + 'info', + 'tip', + 'caution', +] as const; + +export const ADMONITION_BLOCK_REGEX = new RegExp( + `^\\$\\$(${ADMONITION_TYPES.join('|')})\\n([\\s\\S]*?)\\n\\$\\$`, + 'gm' +); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts index 7d8bf8bd6899..94d3a909fa2f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts @@ -27,8 +27,10 @@ import { ExtensionOptions, FileType, } from '../components/BlockEditor/BlockEditor.interface'; +import AdmonitionNode from '../components/BlockEditor/Extensions/AdmonitionNode'; import BlockAndDragDrop from '../components/BlockEditor/Extensions/BlockAndDragDrop/BlockAndDragDrop'; import { Callout } from '../components/BlockEditor/Extensions/Callout/Callout'; +import CodeBlockWithCopy from '../components/BlockEditor/Extensions/CodeBlock/CodeBlockWithCopy'; import DiffView from '../components/BlockEditor/Extensions/diff-view'; import FileNode from '../components/BlockEditor/Extensions/File/FileNode'; import { Focus } from '../components/BlockEditor/Extensions/focus'; @@ -38,8 +40,6 @@ import { LinkExtension } from '../components/BlockEditor/Extensions/link'; import MathEquation from '../components/BlockEditor/Extensions/MathEquation/MathEquation'; import { Mention } from '../components/BlockEditor/Extensions/mention'; import { mentionSuggestion } from '../components/BlockEditor/Extensions/mention/mentionSuggestions'; -import AdmonitionNode from '../components/BlockEditor/Extensions/AdmonitionNode'; -import CodeBlockWithCopy from '../components/BlockEditor/Extensions/CodeBlock/CodeBlockWithCopy'; import SectionNode from '../components/BlockEditor/Extensions/SectionNode'; import slashCommand from '../components/BlockEditor/Extensions/slash-command'; import { getSuggestionItems } from '../components/BlockEditor/Extensions/slash-command/items'; @@ -235,9 +235,7 @@ export class BlockEditorExtensionsClassBase { Document, Paragraph, Text, - ...(coreExtensions - ? this.getCoreExtensions(enableCodeBlockCopy) - : []), + ...(coreExtensions ? this.getCoreExtensions(enableCodeBlockCopy) : []), ...(enableHandlebars ? this.getHandlebarsExtensions() : []), ...(utilityExtensions ? this.getUtilityExtensions() : []), ...(tableExtensions ? this.getTableExtensions() : []), diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx index 88ed2886a0e3..2f04973cb3e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -21,6 +21,7 @@ import { GlobalSettingsMenuCategory, } from '../constants/GlobalSettings.constants'; import { + ADMONITION_BLOCK_REGEX, MARKDOWN_MATCH_ID, SECTION_BLOCK_REGEX, } from '../constants/regex.constants'; @@ -678,12 +679,6 @@ export const validateServiceName = async ( return null; }; -const ADMONITION_TYPES = ['note', 'warning', 'danger', 'info', 'tip', 'caution'] as const; - -const ADMONITION_BLOCK_REGEX = new RegExp( - `^\\$\\$(${ADMONITION_TYPES.join('|')})\\n([\\s\\S]*?)\\n\\$\\$`, - 'gm' -); const convertAdmonitionsToHtml = (markdown: string): string => { ADMONITION_BLOCK_REGEX.lastIndex = 0; @@ -732,7 +727,9 @@ export const processDocMarkdown = (markdown: string): string => { } if (lastIndex < withAdmonitions.length) { - parts.push(MarkdownToHTMLConverter.makeHtml(withAdmonitions.slice(lastIndex))); + parts.push( + MarkdownToHTMLConverter.makeHtml(withAdmonitions.slice(lastIndex)) + ); } return parts.join('\n'); From aa4fb6174c7d632f9705536ea93be6af9ea604ab Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sun, 26 Apr 2026 00:01:17 +0530 Subject: [PATCH 04/11] lint fix --- openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx index 2f04973cb3e7..edc61000a5dd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -679,7 +679,6 @@ export const validateServiceName = async ( return null; }; - const convertAdmonitionsToHtml = (markdown: string): string => { ADMONITION_BLOCK_REGEX.lastIndex = 0; From 3d88b702d29da96ea011b0d12c4cb8e63a703096 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 27 Apr 2026 14:56:05 +0530 Subject: [PATCH 05/11] fixed all the .md files issues --- .../en-US/Dashboard/CustomDashboard.md | 1 + .../public/locales/en-US/Dashboard/Looker.md | 9 ++ .../public/locales/en-US/Database/Athena.md | 6 +- .../locales/en-US/Database/CustomDatabase.md | 1 + .../locales/en-US/Database/Databricks.md | 20 +-- .../locales/en-US/Database/DeltaLake.md | 10 +- .../ui/public/locales/en-US/Database/Doris.md | 1 + .../public/locales/en-US/Database/Exasol.md | 1 + .../ui/public/locales/en-US/Database/Hive.md | 25 ++-- .../ui/public/locales/en-US/Database/Mssql.md | 2 +- .../ui/public/locales/en-US/Database/Mysql.md | 2 +- .../locales/en-US/Database/StarRocks.md | 1 + .../locales/en-US/Database/Timescale.md | 2 +- .../ui/public/locales/en-US/Database/Trino.md | 3 +- .../locales/en-US/Database/UnityCatalog.md | 20 +-- .../en-US/Messaging/CustomMessaging.md | 1 + .../locales/en-US/MlModel/CustomMlModel.md | 1 + .../public/locales/en-US/Pipeline/Airflow.md | 121 +++++++++++------- .../locales/en-US/Pipeline/CustomPipeline.md | 1 + .../ServiceDocPanel/service-doc-panel.less | 4 + .../utils/BlockEditorExtensionsClassBase.ts | 2 +- .../resources/ui/src/utils/ServiceUtils.tsx | 4 +- 22 files changed, 146 insertions(+), 92 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dashboard/CustomDashboard.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dashboard/CustomDashboard.md index b64c868dc0a2..dc925a28c7cb 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dashboard/CustomDashboard.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dashboard/CustomDashboard.md @@ -14,6 +14,7 @@ $$section Source Python Class Name to instantiated by the ingestion workflow. Note that it should implement the `next_record` method so that the Workflow can keep reading and sending records to the OpenMetadata API. +$$ $$section ### Connection Options $(id="connectionOptions") diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dashboard/Looker.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dashboard/Looker.md index 3354ffacbafb..d164ed56e428 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dashboard/Looker.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Dashboard/Looker.md @@ -47,14 +47,21 @@ $$ If we choose to inform the GitHub credentials to ingest LookML Views: +$$section #### Repository Owner $(id="repositoryOwner") The owner (user or organization) of a GitHub repository. For example, in https://github.com/open-metadata/OpenMetadata, the owner is `open-metadata`. +$$ + +$$section #### Repository Name $(id="repositoryName") The name of a GitHub repository. For example, in https://github.com/open-metadata/OpenMetadata, the name is `OpenMetadata`. +$$ + +$$section #### API Token $(id="token") Token to use the API. This is required for private repositories and to ensure we don't hit API limits. @@ -72,3 +79,5 @@ If your GitHub organization has SAML Single Sign-On (SSO) enabled, you must auth Follow these steps to authorize your token for use with SAML SSO. $$ + +$$ diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Athena.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Athena.md index 6475ddded029..a0896e0b6832 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Athena.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Athena.md @@ -48,11 +48,9 @@ And is defined as: ``` -{% note %} - +$$note If you have external services other than glue and facing permission issues, add the permissions to the list above. - -{% /note %} +$$ You can find further information on the Athena connector in the docs. diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/CustomDatabase.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/CustomDatabase.md index cdab34ac4200..fbcdc76030bb 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/CustomDatabase.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/CustomDatabase.md @@ -14,6 +14,7 @@ $$section Source Python Class Name to instantiated by the ingestion workflow. Note that it should implement the `next_record` method so that the Workflow can keep reading and sending records to the OpenMetadata API. +$$ $$section ### Connection Options $(id="connectionOptions") diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Databricks.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Databricks.md index 62b1229f856c..121362dd696a 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Databricks.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Databricks.md @@ -16,13 +16,13 @@ To extract basic metadata (catalogs, schemas, tables, views) from Databricks, th ```sql -- Grant USE CATALOG on catalog -GRANT USE CATALOG ON CATALOG TO ``; +GRANT USE CATALOG ON CATALOG TO ''; -- Grant USE SCHEMA on schemas -GRANT USE SCHEMA ON SCHEMA TO ``; +GRANT USE SCHEMA ON SCHEMA TO ''; -- Grant SELECT on tables and views -GRANT SELECT ON TABLE TO ``; +GRANT SELECT ON TABLE TO ''; ``` ### View Definitions (Optional) @@ -31,7 +31,7 @@ To extract view definitions from `INFORMATION_SCHEMA.VIEWS`, ensure the user has ```sql -- Grant SELECT on INFORMATION_SCHEMA.VIEWS -GRANT SELECT ON VIEW information_schema.views TO ``; +GRANT SELECT ON VIEW information_schema.views TO ''; ``` ### Unity Catalog Tags (Optional) @@ -40,16 +40,16 @@ To extract tags at different levels (catalog, schema, table, column), the user n ```sql -- For catalog-level tags -GRANT SELECT ON TABLE system.information_schema.catalog_tags TO ``; +GRANT SELECT ON TABLE system.information_schema.catalog_tags TO ''; -- For schema-level tags -GRANT SELECT ON TABLE system.information_schema.schema_tags TO ``; +GRANT SELECT ON TABLE system.information_schema.schema_tags TO ''; -- For table-level tags -GRANT SELECT ON TABLE system.information_schema.table_tags TO ``; +GRANT SELECT ON TABLE system.information_schema.table_tags TO ''; -- For column-level tags -GRANT SELECT ON TABLE system.information_schema.column_tags TO ``; +GRANT SELECT ON TABLE system.information_schema.column_tags TO ''; ``` $$note @@ -62,10 +62,10 @@ To extract table and column-level lineage from Unity Catalog system tables, the ```sql -- For table lineage -GRANT SELECT ON TABLE system.access.table_lineage TO ``; +GRANT SELECT ON TABLE system.access.table_lineage TO ''; -- For column lineage -GRANT SELECT ON TABLE system.access.column_lineage TO ``; +GRANT SELECT ON TABLE system.access.column_lineage TO ''; ``` $$note diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/DeltaLake.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/DeltaLake.md index 5a9cdab981f9..3ea7869b4cdc 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/DeltaLake.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/DeltaLake.md @@ -70,28 +70,28 @@ $$section In this configuration we will be pointing to the Hive Metastore database directly. -#### Hive Metastore Database ($id="metastoreDb") +### Hive Metastore Database JDBC connection to the metastore database. It should be a properly formatted database URL, which will be used in the Spark Configuration under `spark.hadoop.javax.jdo.option.ConnectionURL`. -#### Connection UserName ($id="username") +#### Connection UserName Username to use against the metastore database. The value will be used in the Spark Configuration under `spark.hadoop.javax.jdo.option.ConnectionUserName`. -#### Connection Password ($id="password") +#### Connection Password Password to use against metastore database. The value will be used in the Spark Configuration under `spark.hadoop.javax.jdo.option.ConnectionPassword`. -#### Connection Driver Name ($id="driverName") +#### Connection Driver Name Driver class name for JDBC metastore. The value will be used in the Spark Configuration under `spark.hadoop.javax.jdo.option.ConnectionDriverName`, e.g., `org.mariadb.jdbc.Driver`. You will need to provide the driver to the ingestion image, and pass the Class path as explained below. -#### JDBC Driver Class Path ($id="jdbcDriverClassPath") +#### JDBC Driver Class Path Class path to JDBC driver required for the JDBC connection. The value will be used in the Spark Configuration under `spark.driver.extraClassPath`. diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Doris.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Doris.md index 61c1c8279166..8e24f8f5cae8 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Doris.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Doris.md @@ -74,3 +74,4 @@ $$ $$section ### Connection Arguments $(id="connectionArguments") Additional connection arguments such as security or protocol configs that can be sent to the service during connection. +$$ diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Exasol.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Exasol.md index 87d741994f88..a0d68e302d15 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Exasol.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Exasol.md @@ -47,4 +47,5 @@ Uses Transport Layer Security (TLS) but disables the validation of the server ce #### disable-tls Does not use any Transport Layer Security (TLS). Data will be sent in plain text (no encryption). While this may be helpful in rare cases of debugging, make sure you do not use this in production. +$$ diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Hive.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Hive.md index f35b9dd7cf4a..c1a7e7a4965e 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Hive.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Hive.md @@ -33,7 +33,7 @@ This parameter specifies the host and port of the Hive instance. This should be If you are running the OpenMetadata ingestion in a docker and your services are hosted on the `localhost`, then use `host.docker.internal:10000` as the value. $$ -$$section +$$section ### Authentication Mode $(id="auth") The auth parameter specifies the authentication method to use when connecting to the Hive server. Possible values are `LDAP`, `NONE`, `CUSTOM`, or `KERBEROS`. If you are using Kerberos authentication, you should set auth to `KERBEROS`. If you are using custom authentication, you should set auth to `CUSTOM` and provide additional options in the `authOptions` parameter. $$ @@ -50,6 +50,7 @@ $$ $$section ### Database Name $(id="databaseName") + In OpenMetadata, the Database Service hierarchy works as follows: ``` Database Service > Database > Schema > Table @@ -86,10 +87,11 @@ $$ ## Basic Auth +$$section ### Password $(id="password") Password to connect to Postgres/MySQL. - +$$ ## IAM Auth Config @@ -266,35 +268,38 @@ If ticked, the workflow will be able to ingest all database in the cluster. If n ## Hive Mysql Metastore Connection Details - +$$section ### Scheme $(id="scheme") SQLAlchemy driver scheme options. If you are unsure about this setting, you can use the default value. +$$ - - +$$section ### Username $(id="username") Username to connect to MySQL. This user should have access to the `INFORMATION_SCHEMA` to extract metadata. Other workflows may require different permissions -- refer to the section above for more information. +$$ - +$$section ### Host Port $(id="hostPort") This parameter specifies the host and port of the MySQL instance. This should be specified as a string in the format `hostname:port`. For example, you might set the hostPort parameter to `localhost:3306`. If you are running the OpenMetadata ingestion in a docker and your services are hosted on the `localhost`, then use `host.docker.internal:3306` as the value. +$$ - - +$$section ### Database Name $(id="databaseName") + In OpenMetadata, the Database Service hierarchy works as follows: ``` Database Service > Database > Schema > Table ``` In the case of MySQL, we won't have a Database as such. If you'd like to see your data in a database named something other than `default`, you can specify the name in this field. +$$ - - +$$section ### Database Schema $(id="databaseSchema") This is an optional parameter. When set, the value will be used to restrict the metadata reading to a single database (corresponding to the value passed in this field). When left blank, OpenMetadata will scan all the databases. +$$ $$section diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Mssql.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Mssql.md index dbcec1118a5a..839f80782f31 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Mssql.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Mssql.md @@ -30,7 +30,7 @@ Make sure the SQL server that you are trying to connect is in running state. This step allow the sql server to accept remote connection request. -![remote-connection](/doc-images/Database/Mssql/remote-connection.png) +![remote-connection](/locales/doc-images/Database/Mssql/remote-connection.png) #### 3. Configure Windows Firewall diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Mysql.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Mysql.md index 85b90d48913c..7e69d9789028 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Mysql.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Mysql.md @@ -73,7 +73,7 @@ $$ ## IAM Auth Config -$$note +$$note If you are using IAM auth, add
`"ssl": {"ssl-mode": "allow"}` under Connection Arguments $$ diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/StarRocks.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/StarRocks.md index 3fc76683405e..97b1376c151a 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/StarRocks.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/StarRocks.md @@ -74,3 +74,4 @@ $$ $$section ### Connection Arguments $(id="connectionArguments") Additional connection arguments such as security or protocol configs that can be sent to the service during connection. +$$ diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Timescale.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Timescale.md index 641be4ab7171..c7eb608d7ba5 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Timescale.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Timescale.md @@ -35,7 +35,7 @@ $$ ## IAM Auth Config -$$note +$$note If you are using IAM auth, add
`"ssl": {"ssl-mode": "allow"}` under Connection Arguments $$ diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Trino.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Trino.md index 1a2390dabc0a..d254a9395a3c 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Trino.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/Trino.md @@ -29,13 +29,14 @@ $$section Username to connect to Trino. This user should have `SELECT` permission on the `SYSTEM.METADATA` and `INFORMATION_SCHEMA` - see the section above for more details. $$ +$$section ### Auth Config $(id="authType") There are 2 types of auth configs: - Basic Auth. - JWT Auth. User can authenticate the Trino Instance with auth type as `Basic Authentication` i.e. Password **or** by using `JWT Authentication`. - +$$ ## Basic Auth diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/UnityCatalog.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/UnityCatalog.md index 0d657db76def..219096580dac 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/UnityCatalog.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/UnityCatalog.md @@ -16,13 +16,13 @@ To extract basic metadata (catalogs, schemas, tables, views) from Databricks, th ```sql -- Grant USE CATALOG on catalog -GRANT USE CATALOG ON CATALOG TO ``; +GRANT USE CATALOG ON CATALOG TO ''; -- Grant USE SCHEMA on schemas -GRANT USE SCHEMA ON SCHEMA TO ``; +GRANT USE SCHEMA ON SCHEMA TO ''; -- Grant SELECT on tables and views -GRANT SELECT ON TABLE TO ``; +GRANT SELECT ON TABLE TO ''; ``` ### View Definitions (Optional) @@ -31,7 +31,7 @@ To extract view definitions from `INFORMATION_SCHEMA.VIEWS`, ensure the user has ```sql -- Grant SELECT on INFORMATION_SCHEMA.VIEWS -GRANT SELECT ON VIEW information_schema.views TO ``; +GRANT SELECT ON VIEW information_schema.views TO ''; ``` ### Unity Catalog Tags (Optional) @@ -40,16 +40,16 @@ To extract tags at different levels (catalog, schema, table, column), the user n ```sql -- For catalog-level tags -GRANT SELECT ON TABLE system.information_schema.catalog_tags TO ``; +GRANT SELECT ON TABLE system.information_schema.catalog_tags TO ''; -- For schema-level tags -GRANT SELECT ON TABLE system.information_schema.schema_tags TO ``; +GRANT SELECT ON TABLE system.information_schema.schema_tags TO ''; -- For table-level tags -GRANT SELECT ON TABLE system.information_schema.table_tags TO ``; +GRANT SELECT ON TABLE system.information_schema.table_tags TO ''; -- For column-level tags -GRANT SELECT ON TABLE system.information_schema.column_tags TO ``; +GRANT SELECT ON TABLE system.information_schema.column_tags TO ''; ``` $$note @@ -62,10 +62,10 @@ To extract table and column-level lineage from Unity Catalog system tables, the ```sql -- For table lineage -GRANT SELECT ON TABLE system.access.table_lineage TO ``; +GRANT SELECT ON TABLE system.access.table_lineage TO ''; -- For column lineage -GRANT SELECT ON TABLE system.access.column_lineage TO ``; +GRANT SELECT ON TABLE system.access.column_lineage TO ''; ``` $$note diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Messaging/CustomMessaging.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Messaging/CustomMessaging.md index a843adf4752f..81663ebac79a 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Messaging/CustomMessaging.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Messaging/CustomMessaging.md @@ -14,6 +14,7 @@ $$section Source Python Class Name to instantiated by the ingestion workflow. Note that it should implement the `next_record` method so that the Workflow can keep reading and sending records to the OpenMetadata API. +$$ $$section ### Connection Options $(id="connectionOptions") diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/MlModel/CustomMlModel.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/MlModel/CustomMlModel.md index 73c2c3c54fdb..cf03b94f24ca 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/MlModel/CustomMlModel.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/MlModel/CustomMlModel.md @@ -14,6 +14,7 @@ $$section Source Python Class Name to instantiated by the ingestion workflow. Note that it should implement the `next_record` method so that the Workflow can keep reading and sending records to the OpenMetadata API. +$$ $$section ### Connection Options $(id="connectionOptions") diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Pipeline/Airflow.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Pipeline/Airflow.md index 542ed5bb5fbe..ba57180c462a 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Pipeline/Airflow.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Pipeline/Airflow.md @@ -213,14 +213,20 @@ Host and port of the MySQL service. This should be specified as a string in the MySQL schema that contains the Airflow tables. +$$section ### SSL CA $(id="sslCA") Provide the path to SSL CA file, which needs to be local in the ingestion process. +$$ +$$section ### SSL Certificate $(id="sslCert") Provide the path to SSL client certificate file (`ssl_cert`) +$$ +$$section ### SSL Key $(id="sslKey") Provide the path to SSL key file (`ssl_key`) +$$ ## Postgres Connection @@ -239,11 +245,13 @@ Host and port of the Postgres service. E.g., `localhost:5432` or `host.docker.in Postgres database that contains the Airflow tables. +$$section ### SSL Mode $(id="sslMode") SSL Mode to connect to postgres database. E.g, `prefer`, `verify-ca` etc. You can ignore the rest of the properties, since we won't ingest any database not policy tags. +$$ ## MSSQL Connection @@ -271,12 +279,14 @@ $$ ## Basic Auth +$$section ### Password $(id="password") Password to connect to MySQL. +$$ ## IAM Auth Config -$$note +$$note If you are using IAM auth, add
`"ssl": {"ssl-mode": "allow"}` under Connection Arguments $$ @@ -432,6 +442,7 @@ $$ $$section ### Database Name $(id="databaseName") + In OpenMetadata, the Database Service hierarchy works as follows: ``` Database Service > Database > Schema > Table @@ -467,16 +478,18 @@ $$ $$section ### Connection Arguments $(id="connectionArguments") Additional connection arguments such as security or protocol configs that can be sent to the service during connection. +$$ ## Postgres Connection - +$$section ### Username $(id="username") Username to connect to Postgres. This user should have privileges to read all the metadata in Postgres. +$$ - +$$section ### Auth Config $(id="authType") There are 2 types of auth configs: - Basic Auth. @@ -484,18 +497,21 @@ There are 2 types of auth configs: - Azure Based Auth. User can authenticate the Postgres Instance with auth type as `Basic Authentication` i.e. Password **or** by using `IAM based Authentication` to connect to AWS related services **or** by using `Azure Baed Authentication` to connecto to Azure releated services. +$$ ## Basic Auth +$$section ### Password $(id="password") Password to connect to Postgres. +$$ ## IAM Auth Config - +$$section ### AWS Access Key ID $(id="awsAccessKeyId") When you interact with AWS, you specify your AWS security credentials to verify who you are and whether you have permission to access the resources that you are requesting. AWS uses the security credentials to authenticate and authorize your requests (docs). @@ -513,9 +529,9 @@ $$section ### AWS Secret Access Key $(id="awsSecretAccessKey") Secret access key (for example, `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`). +$$ - - +$$section ### AWS Region $(id="awsRegion") Each AWS Region is a separate geographic area in which AWS clusters data centers (docs). @@ -523,25 +539,25 @@ Each AWS Region is a separate geographic area in which AWS clusters data centers As AWS can have instances in multiple regions, we need to know the region the service you want reach belongs to. Note that the AWS Region is the only required parameter when configuring a connection. When connecting to the services programmatically, there are different ways in which we can extract and use the rest of AWS configurations. You can find further information about configuring your credentials here. +$$ - - +$$section ### AWS Session Token $(id="awsSessionToken") If you are using temporary credentials to access your services, you will need to inform the AWS Access Key ID and AWS Secrets Access Key. Also, these will include an AWS Session Token. You can find more information on Using temporary credentials with AWS resources. +$$ - - +$$section ### Endpoint URL $(id="endPointURL") To connect programmatically to an AWS service, you use an endpoint. An *endpoint* is the URL of the entry point for an AWS web service. The AWS SDKs and the AWS Command Line Interface (AWS CLI) automatically use the default endpoint for each service in an AWS Region. But you can specify an alternate endpoint for your API requests. Find more information on AWS service endpoints. +$$ - - +$$section ### Profile Name $(id="profileName") A named profile is a collection of settings and credentials that you can apply to an AWS CLI command. When you specify a profile to run a command, the settings and credentials are used to run that command. Multiple named profiles can be stored in the config and credentials files. @@ -549,9 +565,9 @@ A named profile is a collection of settings and credentials that you can apply t You can inform this field if you'd like to use a profile other than `default`. Find here more information about Named profiles for the AWS CLI. +$$ - - +$$section ### Assume Role ARN $(id="assumeRoleArn") Typically, you use `AssumeRole` within your account or for cross-account access. In this field you'll set the `ARN` (Amazon Resource Name) of the policy of the other account. @@ -561,9 +577,9 @@ A user who wants to access a role in a different account must also have permissi This is a required field if you'd like to `AssumeRole`. Find more information on AssumeRole. +$$ - - +$$section ### Assume Role Session Name $(id="assumeRoleSessionName") An identifier for the assumed role session. Use the role session name to uniquely identify a session when the same role is assumed by different principals or for different reasons. @@ -571,25 +587,26 @@ An identifier for the assumed role session. Use the role session name to uniquel By default, we'll use the name `OpenMetadataSession`. Find more information about the Role Session Name. +$$ - - +$$section ### Assume Role Source Identity $(id="assumeRoleSourceIdentity") The source identity specified by the principal that is calling the `AssumeRole` operation. You can use source identity information in AWS CloudTrail logs to determine who took actions with a role. Find more information about Source Identity. +$$ ## Azure Auth Config - +$$section ### Client ID $(id="clientId") This is a unique identifier for the service account. To fetch this key, look for the value associated with the `client_id` key in the service account key file. +$$ - - +$$section ### Client Secret $(id="clientSecret") To get the client secret, follow these steps: @@ -601,8 +618,9 @@ To get the client secret, follow these steps: 6. In the `Add a client secret` pop-up window, provide a description for your application secret. Choose when the application should expire, and select `Add`. 7. From the `Client secrets` section, copy the string in the `Value` column of the newly created application secret. +$$ - +$$section ### Tenant ID $(id="tenantId") To get the tenant ID, follow these steps: @@ -611,21 +629,21 @@ To get the tenant ID, follow these steps: 2. Search for `App registrations` and select the `App registrations link`. 3. Select the `Azure AD` app you're using for Power BI. 4. From the `Overview` section, copy the `Directory (tenant) ID`. +$$ - - +$$section ### Storage Account Name $(id="accountName") Account Name of your storage account +$$ - - +$$section ### Key Vault Name $(id="vaultName") Key Vault Name +$$ - - +$$section ### Scopes $(id="scopes") To let OM use the Trino Auth APIs using your Azure AD app, you'll need to add the scope @@ -634,23 +652,23 @@ To let OM use the Trino Auth APIs using your Azure AD app, you'll need to add th 3. Select the `Azure AD` app you're using for Trino. 4. From the `Expose an API` section, copy the `Application ID URI` 5. Make sure the URI ends with `/.default` in case it does not, you can append the same manually +$$ - - +$$section ### Host and Port $(id="hostPort") This parameter specifies the host and port of the Postgres instance. This should be specified as a string in the format `hostname:port`. For example, you might set the hostPort parameter to `localhost:5432`. If you are running the OpenMetadata ingestion in a docker and your services are hosted on the `localhost`, then use `host.docker.internal:5432` as the value. +$$ - - +$$section ### Database $(id="database") Initial Postgres database to connect to. If you want to ingest all databases, set `ingestAllDatabases` to true. +$$ - - +$$section ### SSL Mode $(id="sslMode") SSL Mode to connect to postgres database. E.g, `prefer`, `verify-ca`, `allow` etc. @@ -659,14 +677,18 @@ $$note if you are using `IAM auth`, select either `allow` (recommended) or other option based on your use case. $$ +$$ + +$$section ### SSL CA $(id="caCertificate") The CA certificate used for SSL validation (`sslrootcert`). - $$note Postgres only needs CA Certificate $$ +$$ + $$section ### Classification Name $(id="classificationName") @@ -679,43 +701,47 @@ $$section If ticked, the workflow will be able to ingest all database in the cluster. If not ticked, the workflow will only ingest tables from the database set above. $$ - +$$section ### Connection Arguments $(id="connectionArguments") Additional connection arguments such as security or protocol configs that can be sent to service during connection. +$$ - - +$$section ### Connection Options $(id="connectionOptions") Additional connection options to build the URL that can be sent to service during the connection. +$$ ## SQLite Connection +$$section ### Username $(id="username") Username to connect to SQLite. Blank for in-memory database. +$$ - +$$section ### Password $(id="password") Password to connect to SQLite. Blank for in-memory database. +$$ - - +$$section ### Host Port $(id="hostPort") This parameter specifies the host and port of the SQLite instance. This should be specified as a string in the format `hostname:port`. For example, you might set the hostPort parameter to `localhost:3306`. If you are running the OpenMetadata ingestion in a docker and your services are hosted on the `localhost`, then use `host.docker.internal:3306` as the value. Keep it blank for in-memory databases. +$$ - +$$section ### Database $(id="database") Database of the data source. This is an optional parameter, if you would like to restrict the metadata reading to a single database. When left blank, the OpenMetadata Ingestion attempts to scan all the databases. - +$$ $$section ### Database Mode $(id="databaseMode") @@ -723,13 +749,14 @@ $$section How to run the SQLite database. :memory: by default. $$ - +$$section ### Connection Options $(id="connectionOptions") Additional connection options to build the URL that can be sent to service during the connection. +$$ - - +$$section ### Connection Arguments $(id="connectionArguments") -Additional connection arguments such as security or protocol configs that can be sent to service during connection. \ No newline at end of file +Additional connection arguments such as security or protocol configs that can be sent to service during connection. +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Pipeline/CustomPipeline.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Pipeline/CustomPipeline.md index 0630c8ecc69d..2729e567c870 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Pipeline/CustomPipeline.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Pipeline/CustomPipeline.md @@ -14,6 +14,7 @@ $$section Source Python Class Name to instantiated by the ingestion workflow. Note that it should implement the `next_record` method so that the Workflow can keep reading and sending records to the OpenMetadata API. +$$ $$section ### Connection Options $(id="connectionOptions") diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less index 8cfbe1a34c92..fd0b5e04c8f1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less @@ -110,6 +110,10 @@ border-radius: 8px; border-bottom: 2px solid @border-color; background: @markdown-bg-color; + + .ProseMirror-trailingBreak { + display: none; + } } .code-copy-button { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts index 94d3a909fa2f..4b5ff78877dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts @@ -63,7 +63,7 @@ export class BlockEditorExtensionsClassBase { StarterKit.configure({ codeBlock: disableCodeBlock ? false : undefined, heading: { - levels: [1, 2, 3], + levels: [1, 2, 3, 4, 5, 6], }, bulletList: { HTMLAttributes: { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx index edc61000a5dd..986459a7a6af 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -685,7 +685,9 @@ const convertAdmonitionsToHtml = (markdown: string): string => { return markdown.replace( ADMONITION_BLOCK_REGEX, (_match, type: string, content: string) => - `
${content.trim()}
` + `
${MarkdownToHTMLConverter.makeHtml( + content.trim() + )}
` ); }; From e25f1d41edc43927d401e294b63789f72fed7efc Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 27 Apr 2026 22:44:42 +0530 Subject: [PATCH 06/11] addressed gitar comment --- .../BlockEditor/Extensions/AdmonitionNode.ts | 10 ++++++++-- .../ui/src/constants/BlockEditor.constants.ts | 9 +++++++++ .../resources/ui/src/constants/regex.constants.ts | 11 ++--------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts index 6a1ce64a3b59..e25fbc70b0ba 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/AdmonitionNode.ts @@ -11,6 +11,7 @@ * limitations under the License. */ import { mergeAttributes, Node } from '@tiptap/core'; +import { ADMONITION_TYPES } from '../../../constants/BlockEditor.constants'; const AdmonitionNode = Node.create({ name: 'admonition', @@ -22,8 +23,13 @@ const AdmonitionNode = Node.create({ return { type: { default: 'note', - parseHTML: (element) => - element.getAttribute('data-admonition') ?? 'note', + parseHTML: (element) => { + const type = element.dataset.admonition ?? 'note'; + + return (ADMONITION_TYPES as readonly string[]).includes(type) + ? type + : 'note'; + }, renderHTML: (attributes) => ({ 'data-admonition': attributes.type, }), diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/BlockEditor.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/BlockEditor.constants.ts index 26684058efc7..c1b4e13ea7fd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/BlockEditor.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/BlockEditor.constants.ts @@ -75,3 +75,12 @@ export const LINK_PASTE_REGEX = export const UPLOADED_ASSETS_URL = '/api/v1/attachments/'; export const TEXT_TYPES = ['text/plain', 'text/rtf']; + +export const ADMONITION_TYPES = [ + 'note', + 'warning', + 'danger', + 'info', + 'tip', + 'caution', +] as const; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts index 836def549917..be9ee2e167f7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts @@ -11,6 +11,8 @@ * limitations under the License. */ +import { ADMONITION_TYPES } from './BlockEditor.constants'; + export const UrlEntityCharRegEx = /[#.%;?/\\]/g; export const EMAIL_REG_EX = /^\S+@\S+\.\S+$/; @@ -76,15 +78,6 @@ export const IMAGE_URL_PATTERN = export const SECTION_BLOCK_REGEX = /\$\$section\n([\s\S]*?)\n\$\$/g; -const ADMONITION_TYPES = [ - 'note', - 'warning', - 'danger', - 'info', - 'tip', - 'caution', -] as const; - export const ADMONITION_BLOCK_REGEX = new RegExp( `^\\$\\$(${ADMONITION_TYPES.join('|')})\\n([\\s\\S]*?)\\n\\$\\$`, 'gm' From 5a5805d3699b3bd5076d27574639e2d3b3f1d64d Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 28 Apr 2026 16:20:04 +0530 Subject: [PATCH 07/11] added playwright test --- .../e2e/Flow/ServiceDocPanel.spec.ts | 230 ++++++++++++++++++ .../CodeBlock/CodeBlockComponent.tsx | 27 +- .../ServiceDocPanel/service-doc-panel.less | 25 +- 3 files changed, 246 insertions(+), 36 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts new file mode 100644 index 000000000000..de34e3b35eed --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts @@ -0,0 +1,230 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, Page, test } from '@playwright/test'; +import { redirectToHomePage } from '../../utils/common'; +import { waitForAllLoadersToDisappear } from '../../utils/entity'; + +test.use({ storageState: 'playwright/.auth/admin.json' }); + +/** + * Navigates to MySQL service creation step 3 (configure connection), + * where the ServiceDocPanel is visible with code blocks and sections. + */ +const goToMysqlConnectionStep = async (page: Page, serviceName: string) => { + await page.goto('/databaseServices/add-service'); + await waitForAllLoadersToDisappear(page); + await page.getByTestId('Mysql').click(); + await page.getByTestId('next-button').click(); + await page.getByTestId('service-name').fill(serviceName); + await page.getByTestId('next-button').click(); + await page.getByTestId('service-requirements').waitFor({ state: 'visible' }); +}; + +test.describe('ServiceDocPanel', () => { + test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); + }); + + test.describe('Content rendering', () => { + test('should render headings not raw markdown', async ({ page }) => { + await goToMysqlConnectionStep(page, 'pw-doc-panel-headings'); + + const docPanel = page.getByTestId('service-requirements'); + + // Requirements h2 heading should render as an element, not raw "## Requirements" + await expect(docPanel.locator('h2').first()).toBeVisible(); + await expect(docPanel).not.toContainText('## Requirements'); + }); + + test('should render admonition blocks with correct class', async ({ + page, + }) => { + await goToMysqlConnectionStep(page, 'pw-doc-panel-admonition'); + + const docPanel = page.getByTestId('service-requirements'); + + // Mysql.md has $$note blocks — should render as .admonition.admonition-note + const admonition = docPanel.locator('.admonition-note').first(); + + await expect(admonition).toBeVisible(); + // Should contain actual note content, not raw "$$note" syntax + await expect(docPanel).not.toContainText('$$note'); + }); + + test('should render code blocks inside pre > code, not as raw text', async ({ + page, + }) => { + await goToMysqlConnectionStep(page, 'pw-doc-panel-codeblock'); + + const docPanel = page.getByTestId('service-requirements'); + + await expect(docPanel.locator('pre code').first()).toBeVisible(); + // Raw fence markers should not appear + await expect(docPanel).not.toContainText('```'); + }); + + test('should render links that open in a new tab', async ({ page }) => { + await goToMysqlConnectionStep(page, 'pw-doc-panel-links'); + + const docPanel = page.getByTestId('service-requirements'); + const externalLink = docPanel.locator('a[target="_blank"]').first(); + + await expect(externalLink).toBeVisible(); + await expect(externalLink).toHaveAttribute('href', /^https?:\/\//); + }); + + test('should render image in Mssql doc panel', async ({ page }) => { + await page.goto('/databaseServices/add-service'); + await waitForAllLoadersToDisappear(page); + await page.getByTestId('Mssql').click(); + await page.getByTestId('next-button').click(); + await page.getByTestId('service-name').fill('pw-doc-panel-mssql-img'); + await page.getByTestId('next-button').click(); + await page.getByTestId('service-requirements').waitFor({ + state: 'visible', + }); + + const docPanel = page.getByTestId('service-requirements'); + const image = docPanel.locator('img').first(); + + await expect(image).toBeVisible(); + // Verify the image loaded successfully (no broken image) + const naturalWidth = await image.evaluate( + (img: HTMLImageElement) => img.naturalWidth + ); + + expect(naturalWidth).toBeGreaterThan(0); + }); + }); + + test.describe('Section highlighting', () => { + test('should highlight section when the corresponding form field is focused', async ({ + page, + }) => { + await goToMysqlConnectionStep(page, 'pw-doc-panel-highlight'); + + const docPanel = page.getByTestId('service-requirements'); + + // No section should be highlighted initially + await expect( + docPanel.locator('section[data-highlighted="true"]') + ).toHaveCount(0); + + // Focus the username field — activeField becomes "username" + await page.locator(String.raw`#root\/username`).focus(); + + // The username section should now be highlighted + const usernameSection = docPanel.locator( + 'section[data-id="username"][data-highlighted="true"]' + ); + + await expect(usernameSection).toBeVisible(); + }); + + test('should remove highlight from previous section when a new field is focused', async ({ + page, + }) => { + await goToMysqlConnectionStep(page, 'pw-doc-panel-highlight-switch'); + + const docPanel = page.getByTestId('service-requirements'); + + // Focus username first + await page.locator(String.raw`#root\/username`).focus(); + + await expect( + docPanel.locator('section[data-id="username"][data-highlighted="true"]') + ).toBeVisible(); + + // Focus hostPort — username section should lose highlight + await page.locator(String.raw`#root\/hostPort`).focus(); + + await expect( + docPanel.locator('section[data-id="username"][data-highlighted="true"]') + ).toHaveCount(0); + + // hostPort section should now be highlighted + await expect( + docPanel.locator( + 'section[data-id="hostPort"][data-highlighted="true"]' + ) + ).toBeVisible(); + }); + + test('should only ever have one section highlighted at a time', async ({ + page, + }) => { + await goToMysqlConnectionStep(page, 'pw-doc-panel-single-highlight'); + + const docPanel = page.getByTestId('service-requirements'); + + await page.locator(String.raw`#root\/username`).focus(); + await page.locator(String.raw`#root\/hostPort`).focus(); + + await expect( + docPanel.locator('section[data-highlighted="true"]') + ).toHaveCount(1); + }); + + test('should load the correct doc file for the selected service type', async ({ + page, + }) => { + await goToMysqlConnectionStep(page, 'pw-doc-panel-correct-doc'); + + const docPanel = page.getByTestId('service-requirements'); + + // MySQL doc starts with "# MySQL" + await expect(docPanel.locator('h1').first()).toContainText('MySQL'); + }); + }); + + test.describe('Code block copy button', () => { + test('should copy code block content to clipboard and show copied tooltip', async ({ + page, + context, + }) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + await goToMysqlConnectionStep(page, 'pw-doc-panel-copy'); + + const docPanel = page.getByTestId('service-requirements'); + const codeBlock = docPanel.locator('pre').first(); + const copyButtonWrapper = docPanel.locator('.code-copy-button').first(); + const copyButton = docPanel.getByTestId('code-block-copy-icon').first(); + + // Hover code block to reveal the button + await codeBlock.hover(); + await expect(copyButton).toBeVisible(); + + // Verify initial state + await expect(copyButtonWrapper).toHaveAttribute('data-copied', 'false'); + + // Click and verify copied state + tooltip + await copyButton.click(); + + await expect(copyButtonWrapper).toHaveAttribute('data-copied', 'true'); + await expect(page.getByRole('tooltip')).toBeVisible(); + + // Verify clipboard is non-empty + const clipboardText = await page.evaluate(() => + navigator.clipboard.readText() + ); + + expect(clipboardText.length).toBeGreaterThan(0); + + // Verify state resets after 2s timer + await expect(copyButtonWrapper).toHaveAttribute('data-copied', 'false', { + timeout: 3000, + }); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx index a04e4de4c5e7..abcc315e127d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx @@ -11,9 +11,10 @@ * limitations under the License. */ import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react'; +import { Button, Tooltip } from 'antd'; import { FC, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import CopyIcon from '../../../../assets/svg/icon-copy.svg'; +import { ReactComponent as CopyIcon } from '../../../../assets/svg/icon-copy.svg'; const CodeBlockComponent: FC = ({ node }) => { const { t } = useTranslation(); @@ -41,20 +42,18 @@ const CodeBlockComponent: FC = ({ node }) => { return ( - - {t('label.copied')} + + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less index fd0b5e04c8f1..1bc5ca8752e6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/service-doc-panel.less @@ -118,34 +118,15 @@ .code-copy-button { position: absolute; - top: 10px; - right: 10px; - cursor: pointer; + top: 6px; + right: 6px; opacity: 0; transition: opacity 0.2s; - background: none; - border: none; - padding: 0; - line-height: 0; - - &[data-copied='true'] { - opacity: 0.4; - } - } - - .code-copy-message { - position: absolute; - top: 10px; - right: 38px; - font-size: 12px; - color: @text-color; background: @markdown-bg-color; - padding: 2px 6px; border-radius: 4px; - display: none; &[data-copied='true'] { - display: inline; + opacity: 0.4; } } From 237159c5c2866a419584c9154a81153c85cc38e8 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 28 Apr 2026 16:54:04 +0530 Subject: [PATCH 08/11] addressed gitar comment --- .../BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx index abcc315e127d..3819beedce6f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/CodeBlock/CodeBlockComponent.tsx @@ -33,6 +33,9 @@ const CodeBlockComponent: FC = ({ node }) => { try { await navigator.clipboard.writeText(node.textContent); setCopied(true); + if (timerRef.current) { + clearTimeout(timerRef.current); + } timerRef.current = setTimeout(() => setCopied(false), 2000); } catch { // clipboard write failed silently From 223f287bd0acfcdd70db9ee78e4d71095f3ec67b Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 28 Apr 2026 16:56:17 +0530 Subject: [PATCH 09/11] fix lint issue --- .../resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts index de34e3b35eed..681775ac7d7b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts @@ -155,9 +155,7 @@ test.describe('ServiceDocPanel', () => { // hostPort section should now be highlighted await expect( - docPanel.locator( - 'section[data-id="hostPort"][data-highlighted="true"]' - ) + docPanel.locator('section[data-id="hostPort"][data-highlighted="true"]') ).toBeVisible(); }); From 719212b53c98090959146dee5fb2213322d029bc Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 28 Apr 2026 18:32:38 +0530 Subject: [PATCH 10/11] addressed PR comment --- .../e2e/Flow/ServiceDocPanel.spec.ts | 12 +- .../ServiceDocPanel/ServiceDocPanel.test.tsx | 192 +++++++++++++++++- 2 files changed, 196 insertions(+), 8 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts index 681775ac7d7b..b231aa39fb6c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceDocPanel.spec.ts @@ -22,7 +22,9 @@ test.use({ storageState: 'playwright/.auth/admin.json' }); * where the ServiceDocPanel is visible with code blocks and sections. */ const goToMysqlConnectionStep = async (page: Page, serviceName: string) => { - await page.goto('/databaseServices/add-service'); + await page.goto('/databaseServices/add-service', { + waitUntil: 'domcontentloaded', + }); await waitForAllLoadersToDisappear(page); await page.getByTestId('Mysql').click(); await page.getByTestId('next-button').click(); @@ -85,7 +87,9 @@ test.describe('ServiceDocPanel', () => { }); test('should render image in Mssql doc panel', async ({ page }) => { - await page.goto('/databaseServices/add-service'); + await page.goto('/databaseServices/add-service', { + waitUntil: 'domcontentloaded', + }); await waitForAllLoadersToDisappear(page); await page.getByTestId('Mssql').click(); await page.getByTestId('next-button').click(); @@ -220,9 +224,7 @@ test.describe('ServiceDocPanel', () => { expect(clipboardText.length).toBeGreaterThan(0); // Verify state resets after 2s timer - await expect(copyButtonWrapper).toHaveAttribute('data-copied', 'false', { - timeout: 3000, - }); + await expect(copyButtonWrapper).toHaveAttribute('data-copied', 'false'); }); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.test.tsx index 457677b3aa9c..652bdcce1725 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.test.tsx @@ -10,16 +10,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { NodeViewProps } from '@tiptap/core'; +import React from 'react'; import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { fetchMarkdownFile } from '../../../rest/miscAPI'; -import { getActiveFieldNameForAppDocs } from '../../../utils/ServiceUtils'; +import { + getActiveFieldNameForAppDocs, + processDocMarkdown, +} from '../../../utils/ServiceUtils'; +import CodeBlockComponent from '../../BlockEditor/Extensions/CodeBlock/CodeBlockComponent'; import ServiceDocPanel from './ServiceDocPanel'; jest.mock('../Loader/Loader', () => jest.fn().mockReturnValue(
Loader
) ); +jest.mock('@tiptap/react', () => ({ + NodeViewWrapper: ({ + children, + ...props + }: { + children: React.ReactNode; + [key: string]: unknown; + }) => + React.createElement('pre', props, children), + NodeViewContent: ({ as: Tag = 'div' }: { as?: string }) => + React.createElement(Tag), +})); + jest.mock('../../Explore/EntitySummaryPanel/EntitySummaryPanel.component', () => jest .fn() @@ -38,11 +57,17 @@ jest.mock('../../../utils/ServiceUtils', () => ({ })); jest.mock('../RichTextEditor/RichTextEditorPreviewerV1', () => - jest.fn().mockReturnValue(
) + jest.fn(({ markdown }: { markdown: string }) => ( +
+ )) ); jest.mock('react-i18next', () => ({ useTranslation: () => ({ + t: (key: string) => key, i18n: { language: 'en-US', }, @@ -56,6 +81,9 @@ const mockGetActiveFieldNameForAppDocs = getActiveFieldNameForAppDocs as jest.MockedFunction< typeof getActiveFieldNameForAppDocs >; +const mockProcessDocMarkdown = processDocMarkdown as jest.MockedFunction< + typeof processDocMarkdown +>; const mockScrollIntoView = jest.fn(); const mockQuerySelector = jest.fn(); @@ -168,6 +196,48 @@ describe('ServiceDocPanel Component', () => { }); }); + describe('Admonition Rendering', () => { + it('should render a note admonition block', async () => { + mockProcessDocMarkdown.mockReturnValue( + '

This is a note

' + ); + + const { container } = render(); + + await waitFor(() => { + expect( + container.querySelector('.admonition.admonition-note') + ).toBeInTheDocument(); + }); + }); + + it('should render a warning admonition block', async () => { + mockProcessDocMarkdown.mockReturnValue( + '

This is a warning

' + ); + + const { container } = render(); + + await waitFor(() => { + expect( + container.querySelector('.admonition.admonition-warning') + ).toBeInTheDocument(); + }); + }); + + it('should pass fetched markdown through processDocMarkdown', async () => { + mockFetchMarkdownFile.mockResolvedValue('$$note\nsome note\n$$'); + + render(); + + await waitFor(() => { + expect(mockProcessDocMarkdown).toHaveBeenCalledWith( + '$$note\nsome note\n$$' + ); + }); + }); + }); + describe('Field Highlighting', () => { beforeEach(() => { mockQuerySelector.mockReturnValue(createMockElement()); @@ -328,3 +398,119 @@ describe('ServiceDocPanel Component', () => { }); }); }); + +describe('CodeBlockComponent', () => { + const mockWriteText = jest.fn().mockResolvedValue(undefined); + + const mockNode = { + textContent: 'SELECT * FROM table;', + } as unknown as NodeViewProps['node']; + + const mockNodeViewProps = { + node: mockNode, + } as unknown as NodeViewProps; + + beforeEach(() => { + jest.clearAllMocks(); + Object.defineProperty(navigator, 'clipboard', { + value: { writeText: mockWriteText }, + writable: true, + }); + }); + + it('should render the copy button', () => { + render(); + + expect(screen.getByTestId('code-block-copy-icon')).toBeInTheDocument(); + }); + + it('should set data-copied to false initially', () => { + const { container } = render(); + + expect( + container.querySelector('.code-copy-button') + ).toHaveAttribute('data-copied', 'false'); + }); + + it('should copy node text content to clipboard on click', async () => { + render(); + + fireEvent.click(screen.getByTestId('code-block-copy-icon')); + + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledWith('SELECT * FROM table;'); + }); + }); + + it('should set data-copied to true after clicking copy', async () => { + const { container } = render(); + + fireEvent.click(screen.getByTestId('code-block-copy-icon')); + + await waitFor(() => { + expect( + container.querySelector('.code-copy-button') + ).toHaveAttribute('data-copied', 'true'); + }); + }); + + it('should schedule a timer to reset data-copied after clicking copy', async () => { + const setTimeoutSpy = jest.spyOn(global, 'setTimeout'); + + const { container } = render(); + + fireEvent.click(screen.getByTestId('code-block-copy-icon')); + + await waitFor(() => { + expect( + container.querySelector('.code-copy-button') + ).toHaveAttribute('data-copied', 'true'); + }); + + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 2000); + + setTimeoutSpy.mockRestore(); + }); + + it('should cancel the previous timer on rapid clicks', async () => { + const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); + const { container } = render(); + + fireEvent.click(screen.getByTestId('code-block-copy-icon')); + + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledTimes(1); + }); + + fireEvent.click(screen.getByTestId('code-block-copy-icon')); + + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledTimes(2); + }); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + + expect( + container.querySelector('.code-copy-button') + ).toHaveAttribute('data-copied', 'true'); + + clearTimeoutSpy.mockRestore(); + }); + + it('should clear timeout on unmount', async () => { + const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); + const { unmount } = render(); + + fireEvent.click(screen.getByTestId('code-block-copy-icon')); + + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalled(); + }); + + unmount(); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + + clearTimeoutSpy.mockRestore(); + }); +}); From ac8c6cb9bafd6c5acb9dd27b63b52066d602fdd0 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 28 Apr 2026 18:58:19 +0530 Subject: [PATCH 11/11] lint fix --- .../ServiceDocPanel/ServiceDocPanel.test.tsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.test.tsx index 652bdcce1725..c6cb6707e996 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ServiceDocPanel/ServiceDocPanel.test.tsx @@ -33,8 +33,7 @@ jest.mock('@tiptap/react', () => ({ }: { children: React.ReactNode; [key: string]: unknown; - }) => - React.createElement('pre', props, children), + }) => React.createElement('pre', props, children), NodeViewContent: ({ as: Tag = 'div' }: { as?: string }) => React.createElement(Tag), })); @@ -427,9 +426,10 @@ describe('CodeBlockComponent', () => { it('should set data-copied to false initially', () => { const { container } = render(); - expect( - container.querySelector('.code-copy-button') - ).toHaveAttribute('data-copied', 'false'); + expect(container.querySelector('.code-copy-button')).toHaveAttribute( + 'data-copied', + 'false' + ); }); it('should copy node text content to clipboard on click', async () => { @@ -448,9 +448,10 @@ describe('CodeBlockComponent', () => { fireEvent.click(screen.getByTestId('code-block-copy-icon')); await waitFor(() => { - expect( - container.querySelector('.code-copy-button') - ).toHaveAttribute('data-copied', 'true'); + expect(container.querySelector('.code-copy-button')).toHaveAttribute( + 'data-copied', + 'true' + ); }); }); @@ -462,9 +463,10 @@ describe('CodeBlockComponent', () => { fireEvent.click(screen.getByTestId('code-block-copy-icon')); await waitFor(() => { - expect( - container.querySelector('.code-copy-button') - ).toHaveAttribute('data-copied', 'true'); + expect(container.querySelector('.code-copy-button')).toHaveAttribute( + 'data-copied', + 'true' + ); }); expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 2000); @@ -490,9 +492,10 @@ describe('CodeBlockComponent', () => { expect(clearTimeoutSpy).toHaveBeenCalled(); - expect( - container.querySelector('.code-copy-button') - ).toHaveAttribute('data-copied', 'true'); + expect(container.querySelector('.code-copy-button')).toHaveAttribute( + 'data-copied', + 'true' + ); clearTimeoutSpy.mockRestore(); });