From 675a48e2b44cb4b72e9ba69cbb3adccb7ae292b5 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Tue, 31 Mar 2026 16:45:07 +0530 Subject: [PATCH 01/12] fix(ui): remove @toast-ui/react-editor dependency and migrate to BlockEditor --- .../src/main/resources/ui/package.json | 1 - .../ArticleViewer.component.tsx | 4 +- .../SettingsSso/SSODocPanel/SSODocPanel.tsx | 4 +- .../CustomHtmlRederer.interface.ts | 43 - .../CustomHtmlRederer/CustomHtmlRederer.tsx | 335 ------- .../CustomHtmlRenderer.test.ts | 53 -- .../RichTextEditorPreviewer.test.tsx | 876 ------------------ .../RichTextEditorPreviewer.tsx | 145 --- .../rich-text-editor-previewer.less | 324 ------- .../ServiceDocPanel/ServiceDocPanel.tsx | 57 +- .../ServiceDocPanel/service-doc-panel.less | 120 ++- .../src/main/resources/ui/yarn.lock | 42 +- 12 files changed, 181 insertions(+), 1823 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRederer.interface.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRederer.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRenderer.test.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewer.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewer.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/rich-text-editor-previewer.less diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index 1cb59eb05cf8..291e157e53af 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -97,7 +97,6 @@ "@tiptap/react": "^2.3.0", "@tiptap/starter-kit": "^2.3.0", "@tiptap/suggestion": "^2.3.0", - "@toast-ui/react-editor": "^3.1.8", "@untitledui/icons": "^0.0.21", "@windmillcode/quill-emoji": "2.0.3000", "analytics": "^0.8.1", diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx index 4a9ff2e04116..646d31d32291 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.component.tsx @@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next'; import { LearningResource } from '../../../rest/learningResourceAPI'; import { getSanitizeContent } from '../../../utils/sanitize.utils'; import { showErrorToast } from '../../../utils/ToastUtils'; -import RichTextEditorPreviewer from '../../common/RichTextEditor/RichTextEditorPreviewer'; +import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import './article-viewer.less'; interface ArticleViewerProps { @@ -106,7 +106,7 @@ export const ArticleViewer: React.FC = ({ resource }) => {
- diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.tsx index 46c83f63a88c..b2bdf2fbb434 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.tsx @@ -26,7 +26,7 @@ import { getProviderIcon, } from '../../../utils/SSOUtils'; import Loader from '../../common/Loader/Loader'; -import RichTextEditorPreviewer from '../../common/RichTextEditor/RichTextEditorPreviewer'; +import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; import './sso-doc-panel.less'; import { FIELD_MAPPINGS, PROVIDER_FILE_MAP } from './SSODocPanel.constants'; @@ -251,7 +251,7 @@ const SSODocPanel: FC = ({ serviceName, activeField }) => { )}`}
- diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRederer.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRederer.interface.ts deleted file mode 100644 index 3a09d70b0148..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRederer.interface.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023 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. - */ - -export type TokenAttrs = Record; -export interface TagToken { - tagName: string; - outerNewLine?: boolean; - innerNewLine?: boolean; -} - -export interface OpenTagToken extends TagToken { - type: 'openTag'; - classNames?: string[]; - attributes?: TokenAttrs; - selfClose?: boolean; -} - -export interface CloseTagToken extends TagToken { - type: 'closeTag'; -} - -export interface TextToken { - type: 'text'; - content: string; -} - -export interface RawHTMLToken { - type: 'html'; - content: string; - outerNewLine?: boolean; -} - -export type HTMLToken = OpenTagToken | CloseTagToken | TextToken | RawHTMLToken; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRederer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRederer.tsx deleted file mode 100644 index b7559ac3f90b..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRederer.tsx +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright 2023 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. - */ - -/** - * This file contains code for custom renderers read more - * @see {@link https://github.com/nhn/tui.editor/blob/master/docs/en/custom-html-renderer.md} - */ - -import { - CodeBlockMdNode, - CustomHTMLRenderer, - HeadingMdNode, - LinkMdNode, - MdNode, -} from '@toast-ui/editor'; -import CodeMirror from 'codemirror'; -import 'codemirror/addon/runmode/runmode'; -import 'codemirror/mode/clike/clike'; -import 'codemirror/mode/javascript/javascript'; -import 'codemirror/mode/python/python'; -import 'codemirror/mode/sql/sql'; -import 'codemirror/mode/yaml/yaml'; - -import katex from 'katex'; -import React from 'react'; -import ReactDOMServer from 'react-dom/server'; -import CopyIcon from '../../../../assets/svg/icon-copy.svg'; -import { - markdownTextAndIdRegex, - MARKDOWN_MATCH_ID, -} from '../../../../constants/regex.constants'; -import { MarkdownToHTMLConverter } from '../../../../utils/FeedUtils'; -import i18n from '../../../../utils/i18next/LocalUtil'; -import { - HTMLToken, - OpenTagToken, - TextToken, -} from './CustomHtmlRederer.interface'; - -const getHTMLTokens = (node: MdNode): HTMLToken[] => { - const blockNode = node as CodeBlockMdNode; - - // Parse inline markdown to html string - const htmlContent = MarkdownToHTMLConverter.makeHtml(blockNode.literal ?? ''); - - return [ - { - type: 'openTag', - tagName: 'div', - outerNewLine: true, - classNames: ['admonition', `admonition_${blockNode.info}`], - }, - { - type: 'html', - content: htmlContent, - outerNewLine: true, - }, - { type: 'closeTag', tagName: 'div', outerNewLine: true }, - ]; -}; - -export const CodeMirrorLanguageAliases: Readonly> = { - // Mappings for C-like languages https://codemirror.net/5/mode/clike/index.html - c: 'text/x-csrc', - 'c++': 'text/x-c++src', - java: 'text/x-java', - csharp: 'text/x-csharp', - scala: 'text/x-scala', - kotlin: 'text/x-kotlin', - objectivec: 'text/x-objectivec', - 'objectivec++': 'text/x-objectivec++', - // Aliases for convenience - js: 'javascript', - py: 'python', - cpp: 'text/x-c++src', -}; - -export const customHTMLRenderer: CustomHTMLRenderer = { - note(node) { - return getHTMLTokens(node); - }, - warning(node) { - return getHTMLTokens(node); - }, - danger(node) { - return getHTMLTokens(node); - }, - info(node) { - return getHTMLTokens(node); - }, - htmlInline(_, { origin }) { - // This handles inline HTML elements like - const originResult = origin && origin(); - - return originResult || null; - }, - tip(node) { - return getHTMLTokens(node); - }, - caution(node) { - return getHTMLTokens(node); - }, - codeBlock(node) { - const { fenceLength, info } = node as CodeBlockMdNode; - const infoWords = info ? info.split(/\s+/) : []; - const preClasses = ['relative', 'code-block']; - - const codeAttrs: Record = {}; - - const codeText = node.literal ?? ''; - - if (fenceLength > 3) { - codeAttrs['data-backticks'] = fenceLength; - } - const lang = (infoWords?.[0] && infoWords[0]) || null; - const codeFragments: React.ReactElement[] = []; - if (codeText && lang) { - // normalize CodeMirror language (mode) specifier - const cmLang = CodeMirrorLanguageAliases[lang] || lang; - - // set attributes - preClasses.push('cm-s-default', `lang-${cmLang}`); - codeAttrs['data-language'] = cmLang; - - // apply highlight - CodeMirror.runMode(codeText, cmLang, (text, style) => { - if (style) { - const className = style - .split(/\s+/g) - .map((s) => `cm-${s}`) - .join(' '); - codeFragments.push({text}); - } else { - codeFragments.push({text}); - } - }); - } else { - // plain code block - codeFragments.push({codeText}); - } - - return [ - { - type: 'openTag', - tagName: 'pre', - classNames: preClasses, - }, - { - type: 'html', - content: ReactDOMServer.renderToString( - <> - {...codeFragments} - - {i18n.t('label.copied').toString()} - - - - ), - }, - { type: 'closeTag', tagName: 'pre' }, - ]; - }, - link(node, { origin, entering }) { - const linkNode = node as LinkMdNode; - - // get the origin result - const originResult = (origin && origin()) as OpenTagToken; - - // get the attributes - const attributes = originResult.attributes ?? {}; - - // derive the target - const target = linkNode.destination?.startsWith('#') ? '_self' : '_blank'; - - if (entering) { - originResult.attributes = { - ...attributes, - target, - }; - } - - return originResult; - }, - heading(node, { entering, origin, getChildrenText }) { - // get the origin result - const originResult = (origin && origin()) as OpenTagToken; - - // get the attributes - const attributes = originResult.attributes ?? {}; - - const headingNode = node as HeadingMdNode; - const childrenText = getChildrenText(headingNode); - - /** - * create an id from the child text without any space and punctuation - * and make it lowercase for bookmarking - * @example (Postgres) will be postgres - */ - let id = childrenText - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .toLowerCase(); - - const match = childrenText.match(markdownTextAndIdRegex); - - // if id regex matched then override the id with matched ID - if (match) { - id = match[2]; - } - - // if it is a opening tag - if (entering) { - originResult.attributes = { - ...attributes, - id, - 'data-highlighted': 'false', - }; - } - - return originResult; - }, - text(node) { - let nodeText = ''; - const nodeLiteral = node.literal ?? ''; - - // check if node literal has id and text - const match = nodeLiteral.match(markdownTextAndIdRegex); - - // get the text only (without $(id="some_value")) - if (match) { - nodeText = match[1]; - } else { - nodeText = nodeLiteral; - } - - return { - type: 'text', - content: nodeText, - } as TextToken; - }, - section(node) { - const blockNode = node as CodeBlockMdNode; - let literal = blockNode.literal ?? ''; - - let id = ''; - - // check if node literal has id - const match = literal.match(MARKDOWN_MATCH_ID); - - if (match) { - // replace the id text with empty string - // $(id="schema") --> '' - // we have to do this as we don't want to render the id text - literal = literal.replace(match[0], ''); - - // store the actual id - id = match[1]; - } - - // Parse inline markdown to html string - const htmlContent = MarkdownToHTMLConverter.makeHtml(literal); - - return [ - { - type: 'openTag', - tagName: 'section', - attributes: { - 'data-id': id, - 'data-highlighted': 'false', - }, - }, - { - type: 'html', - content: htmlContent, - outerNewLine: true, - }, - { type: 'closeTag', tagName: 'section', outerNewLine: true }, - ]; - }, - - latex(node) { - const content = katex.renderToString(node.literal ?? '', { - throwOnError: false, - output: 'mathml', - }); - - return [ - { type: 'openTag', tagName: 'div', outerNewLine: true }, - { type: 'html', content: content }, - { type: 'closeTag', tagName: 'div', outerNewLine: true }, - ]; - }, -}; - -export const replaceLatex = (content: string) => { - try { - const latexPattern = /\$\$latex[\s\S]*?\$\$/g; - const latexContentPattern = /\$\$latex\s*([\s\S]*?)\s*\$\$/g; - - return content.replace(latexPattern, (latex) => { - const matches = [...latex.matchAll(latexContentPattern)]; - - if (matches.length === 0) { - return latex; - } - - return katex.renderToString(matches[0][1] ?? '', { - throwOnError: false, - output: 'mathml', - }); - }); - } catch (error) { - return content; - } -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRenderer.test.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRenderer.test.ts deleted file mode 100644 index 1359f4c69d8d..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/CustomHtmlRederer/CustomHtmlRenderer.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2024 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. - */ -/* eslint-disable max-len */ -import { replaceLatex } from './CustomHtmlRederer'; - -jest.mock('katex', () => ({ - renderToString: jest.fn().mockReturnValue('latex content'), -})); - -describe('replaceLatex', () => { - it('returns the original string if no LaTeX is present', () => { - const input = 'This is a test string without LaTeX.'; - const output = replaceLatex(input); - - expect(output).toBe(input); - }); - - it('replaces LaTeX content correctly', () => { - const content = - 'This dimension table contains a row for each channel or app that your customers use to create orders. Some examples of these include Facebook and Online Store. You can join this table with the sales table to measure channel performance.\n\n$$latex\n\text{$dfrac{NetSales}{Quantity Invoiced}=Average Price, :$ If QuantityInvoiced is Null then,$: AveragePrice=Null$}\n$$'; - - const output = replaceLatex(content); - - expect(output).toContain('latex content'); - - expect(output).not.toContain('$$latex'); - }); - - it('replaces multiple LaTeX contents correctly', () => { - const content = `$$latex \\frac{a}{b}$$ $$latex \\frac{c}{d}$$`; - const output = replaceLatex(content); - - expect(output).toContain('latex content latex content'); - }); - - it('returns the original LaTeX string if it is malformed', () => { - const malformedLatex = '$$latex \\frac{a}{b}'; - const input = `This is malformed: ${malformedLatex}`; - const output = replaceLatex(input); - - expect(output).toContain(malformedLatex); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewer.test.tsx deleted file mode 100644 index c787f5c2bf78..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewer.test.tsx +++ /dev/null @@ -1,876 +0,0 @@ -/* - * Copyright 2022 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 { - act, - findByTestId, - fireEvent, - render, - screen, -} from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; - -import { CodeMirrorLanguageAliases } from './CustomHtmlRederer/CustomHtmlRederer'; -import { PreviewerProp } from './RichTextEditor.interface'; -import RichTextEditorPreviewer from './RichTextEditorPreviewer'; - -const mockDescription = - '**Headings**\n\n# H1\n## H2\n### H3\n\n***\n**Bold**\n\n**bold text**\n\n\n***\n**Italic**\n\n*italic*\n\n***\n**BlockQuote**\n\n> blockquote\n\n***\n**Ordered List**\n\n1. First item\n2. Second item\n3. Third item\n\n\n***\n**Unordered List**\n\n- First item\n- Second item\n- Third item\n\n\n***\n**Code**\n\n`code`\n\n\n***\n**Horizontal Rule**\n\n---\n\n\n***\n**Link**\n[title](https://www.example.com)\n\n\n***\n**Image**\n\n![alt text](https://github.com/open-metadata/OpenMetadata/blob/main/docs/.gitbook/assets/openmetadata-banner.png?raw=true)\n\n\n***\n**Table**\n\n| Syntax | Description |\n| ----------- | ----------- |\n| Header | Title |\n| Paragraph | Text |\n***\n\n**Fenced Code Block**\n\n```\n{\n "firstName": "John",\n "lastName": "Smith",\n "age": 25\n}\n```\n\n\n***\n**Strikethrough**\n~~The world is flat.~~\n'; - -const mockCodeBlockMarkdown = - // eslint-disable-next-line max-len - "```\nIFERROR ( \n IF (\n SUM ( 'Запасы'[СЗ, руб2] ) <> BLANK (),\n CALCULATE (\n DIVIDE ( SUM ( 'Запасы'[СЗ, руб2] ), [Количество дней в периоде_new] ),\n FILTER ( 'Место отгрузки', [Код предприятия] <> \"7001\" ),\n FILTER ( 'Запасы', [Код типа запаса] <> \"E\" )\n ),\n BLANK ()\n ),\n 0\n)\n```"; - -const mockProp: PreviewerProp = { - markdown: mockDescription, - className: '', - maxLength: 300, - enableSeeMoreVariant: true, - isDescriptionExpanded: false, -}; - -describe('Test RichTextEditor Previewer Component', () => { - it('Should render RichTextEditorViewer Component', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const viewerContainer = await findByTestId(container, 'viewer-container'); - - expect(viewerContainer).toBeInTheDocument(); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render bold markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const boldMarkdown = markdownParser.querySelectorAll('strong'); - - expect(boldMarkdown).toHaveLength(boldMarkdown.length); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render strikethrough markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser.querySelector('del')).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('del')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render headings markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const heading1 = markdownParser.querySelector('h1'); - const heading2 = markdownParser.querySelector('h2'); - const heading3 = markdownParser.querySelector('h3'); - - expect(heading1).not.toBeInTheDocument(); - expect(heading2).not.toBeInTheDocument(); - expect(heading3).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('h1')).toBeInTheDocument(); - expect(markdownParser.querySelector('h2')).toBeInTheDocument(); - expect(markdownParser.querySelector('h3')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render italic markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const italicMarkdown = markdownParser.querySelector('em'); - - expect(italicMarkdown).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('em')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render blockquote markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const blockquoteMarkdown = markdownParser.querySelector('blockquote'); - - expect(blockquoteMarkdown).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('blockquote')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render ordered list markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const orderedList = markdownParser.querySelector('ol'); - - expect(orderedList).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('ol')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render unordered list markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const unorderedList = markdownParser.querySelector('ul'); - - expect(unorderedList).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('ul')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render code markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const code = markdownParser.querySelector('code'); - - expect(code).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('code')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render code block markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser.querySelector('pre')).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('pre')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it.each([ - ['javascript', 'const foo = "bar";'], - ['js', 'const foo = "bar";'], - ['java', 'public static string foo = "bar";'], - ['text/x-java', 'public static string foo = "bar";'], - ['python', 'foo = "bar"'], - ['py', 'foo = "bar"'], - ['sql', 'SELECT "bar" AS foo'], - ['yaml', 'foo: bar'], - ])( - 'Should render code block (%s) markdown content', - async (language, content) => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - const cmLang = CodeMirrorLanguageAliases[language] || language; - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser).toBeInTheDocument(); - - // pre - const pre = markdownParser.querySelector('pre.code-block'); - - expect(pre).toBeInTheDocument(); - expect(pre).toHaveClass('cm-s-default', `lang-${cmLang}`); - - // code - const code = pre?.querySelector('code'); - - expect(code).toBeInTheDocument(); - expect(code).toHaveAttribute('data-language', cmLang); - - // code fragment - expect(code?.querySelector('span')).toBeInTheDocument(); - } - ); - - it('Should render code block (unsupported language) markdown content', async () => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser).toBeInTheDocument(); - - // pre - const pre = markdownParser.querySelector('pre.code-block'); - - expect(pre).toBeInTheDocument(); - expect(pre).toHaveClass('cm-s-default', `lang-unknown`); - - // code - const code = pre?.querySelector('code'); - - expect(code).toBeInTheDocument(); - expect(code).toHaveAttribute('data-language', 'unknown'); - - // no code fragments - expect(code?.querySelector('span')).not.toBeInTheDocument(); - }); - - it('Should render code block (without language specifier) markdown content', async () => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser).toBeInTheDocument(); - - // pre - const pre = markdownParser.querySelector('pre.code-block'); - - expect(pre).toBeInTheDocument(); - expect(pre).not.toHaveClass('cm-s-default'); - expect( - [...(pre?.classList || [])].find((c) => c.startsWith('lang-')) - ).toBeUndefined(); - - // code - const code = pre?.querySelector('code'); - - expect(code).toBeInTheDocument(); - expect(code).not.toHaveAttribute('data-language'); - - // no code fragments - expect(code?.querySelector('span')).not.toBeInTheDocument(); - }); - - it('Should render horizontal rule markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const horizontalRule = markdownParser.querySelector('hr'); - - expect(horizontalRule).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('hr')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render link markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - const link = markdownParser.querySelector('a'); - - expect(link).toBeNull(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('a')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render image markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser.querySelector('img')).toBeNull(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('img')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render table markdown content', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser.querySelector('table')).not.toBeInTheDocument(); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - - act(() => { - fireEvent.click(readMoreButton); - }); - - expect(markdownParser.querySelector('table')).toBeInTheDocument(); - - expect(markdownParser).toBeInTheDocument(); - }); - - it('Should render read more button if enableSeeMoreVariant is true and max length is less than content length', () => { - render(, { - wrapper: MemoryRouter, - }); - - expect(screen.getByTestId('read-more-button')).toBeInTheDocument(); - }); - - it('Read more toggling should work', async () => { - render(, { - wrapper: MemoryRouter, - }); - - const readMoreButton = screen.getByTestId('read-more-button'); - - fireEvent.click(readMoreButton); - - const readLessButton = screen.getByTestId('read-less-button'); - - expect(readLessButton).toBeInTheDocument(); - - fireEvent.click(readLessButton); - - expect(screen.getByTestId('read-more-button')).toBeInTheDocument(); - }); - - it('Should render the whole content if enableSeeMoreVariant is false', () => { - const markdown = 'This is a simple paragraph text'; - - render( - , - { - wrapper: MemoryRouter, - } - ); - - expect(screen.getByText(markdown)).toBeInTheDocument(); - expect(screen.queryByTestId('read-more-button')).toBeNull(); - }); - - it('Should render the clipped content if enableSeeMoreVariant is true', () => { - const markdown = 'This is a simple paragraph text'; - - render( - , - { - wrapper: MemoryRouter, - } - ); - - expect(screen.getByText('This is a simple...')).toBeInTheDocument(); - expect(screen.queryByTestId('read-more-button')).toBeInTheDocument(); - }); - - it('Should not clipped content if enableSeeMoreVariant is true and markdown length is less than max length', () => { - const markdown = 'This is a simple paragraph text'; - - render( - , - { - wrapper: MemoryRouter, - } - ); - - expect(screen.getByText(markdown)).toBeInTheDocument(); - expect(screen.queryByTestId('read-more-button')).toBeNull(); - }); - - it('Should render code block with copy button', async () => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser.querySelector('pre')).toBeInTheDocument(); - - expect(screen.getByTestId('code-block-copy-icon')).toBeInTheDocument(); - }); - - it('Should render read less button if isDescriptionExpanded is true', async () => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - - const readLessButton = await findByTestId(container, 'read-less-button'); - - expect(readLessButton).toBeInTheDocument(); - }); - - it('Should render read more button if isDescriptionExpanded is false', async () => { - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const readMoreButton = await findByTestId(container, 'read-more-button'); - - expect(readMoreButton).toBeInTheDocument(); - }); - - it('Should handle clipboard copy on code block copy icon click', async () => { - Object.assign(navigator, { - clipboard: { - writeText: jest.fn().mockResolvedValue(undefined), - }, - }); - - const { container } = render( - , - { wrapper: MemoryRouter } - ); - - const copyIcon = await findByTestId(container, 'code-block-copy-icon'); - - fireEvent.mouseDown(copyIcon); - - expect(navigator.clipboard.writeText).toHaveBeenCalled(); - }); - - it('Should set data-copied attribute to true after copying', async () => { - jest.useFakeTimers(); - - Object.assign(navigator, { - clipboard: { - writeText: jest.fn().mockResolvedValue(undefined), - }, - }); - - const { container } = render( - , - { wrapper: MemoryRouter } - ); - - const copyIcon = await findByTestId(container, 'code-block-copy-icon'); - - await act(async () => { - fireEvent.mouseDown(copyIcon); - }); - - jest.advanceTimersByTime(2000); - - jest.useRealTimers(); - }); - - it('Should apply custom className to container', () => { - const customClass = 'custom-previewer-class'; - render(, { - wrapper: MemoryRouter, - }); - - const container = screen.getByTestId('viewer-container'); - - expect(container).toHaveClass('rich-text-editor-container', customClass); - }); - - it('Should apply textVariant className to markdown-parser', async () => { - const { container } = render( - , - { wrapper: MemoryRouter } - ); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser).toHaveClass('white'); - }); - - it('Should apply reducePreviewLineClass when not expanded', async () => { - const { container } = render( - , - { wrapper: MemoryRouter } - ); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser).toHaveClass('custom-reduce-class'); - }); - - it('Should not apply reducePreviewLineClass when expanded', async () => { - const { container } = render( - , - { wrapper: MemoryRouter } - ); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser).not.toHaveClass('custom-reduce-class'); - }); - - it('Should render with RTL direction when i18n.dir() returns rtl', () => { - jest.spyOn(require('react-i18next'), 'useTranslation').mockReturnValue({ - t: (key: string) => key, - i18n: { dir: () => 'rtl' }, - }); - - render(, { - wrapper: MemoryRouter, - }); - - const container = screen.getByTestId('viewer-container'); - - expect(container).toHaveClass('text-right'); - expect(container).toHaveAttribute('dir', 'rtl'); - }); - - it('Should not show read more button when showReadMoreBtn is false', () => { - render(, { - wrapper: MemoryRouter, - }); - - expect(screen.queryByTestId('read-more-button')).not.toBeInTheDocument(); - }); - - it('Should format HTML content using formatContent utility', () => { - const htmlMarkdown = '

Test HTML content

'; - render(, { - wrapper: MemoryRouter, - }); - - expect(screen.getByTestId('viewer-container')).toBeInTheDocument(); - }); - - it('Should handle markdown without HTML tags', () => { - const plainMarkdown = 'Plain text without any HTML'; - render(, { - wrapper: MemoryRouter, - }); - - expect(screen.getByText(plainMarkdown)).toBeInTheDocument(); - }); - - it('Should render latex content using replaceLatex', () => { - const latexMarkdown = '$$latex \\frac{a}{b}$$'; - render(, { - wrapper: MemoryRouter, - }); - - expect(screen.getByTestId('viewer-container')).toBeInTheDocument(); - }); - - it('Should handle clipboard copy errors gracefully', async () => { - const consoleErrorSpy = jest - .spyOn(console, 'error') - .mockImplementation(() => {}); - - Object.assign(navigator, { - clipboard: { - writeText: jest.fn().mockRejectedValue(new Error('Copy failed')), - }, - }); - - const { container } = render( - , - { wrapper: MemoryRouter } - ); - - const copyIcon = await findByTestId(container, 'code-block-copy-icon'); - - await act(async () => { - fireEvent.mouseDown(copyIcon); - }); - - consoleErrorSpy.mockRestore(); - }); - - it('Should render with default maxLength when not specified', () => { - const { maxLength: _maxLength, ...propsWithoutMaxLength } = mockProp; - render(, { - wrapper: MemoryRouter, - }); - - expect(screen.getByTestId('viewer-container')).toBeInTheDocument(); - }); - - it('Should cleanup mousedown event listener on unmount', () => { - const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); - const { unmount } = render(, { - wrapper: MemoryRouter, - }); - - unmount(); - - expect(removeEventListenerSpy).toHaveBeenCalledWith( - 'mousedown', - expect.any(Function) - ); - - removeEventListenerSpy.mockRestore(); - }); - - it('Should render unique Viewer component key for each render', () => { - const { rerender } = render( - , - { wrapper: MemoryRouter } - ); - - const firstRender = screen.getByTestId('markdown-parser'); - - rerender( - - ); - - const secondRender = screen.getByTestId('markdown-parser'); - - expect(firstRender).toBe(secondRender); - }); - - it('Should render link with target="_blank" attribute', async () => { - const markdownWithLink = '[External Link](https://example.com)'; - const { container } = render( - , - { wrapper: MemoryRouter } - ); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - const link = markdownParser.querySelector('a'); - - expect(link).toHaveAttribute('target', '_blank'); - }); - - it('Should handle extendedAutolinks in Viewer', () => { - const markdownWithAutolink = 'Visit https://example.com for more info'; - render( - , - { wrapper: MemoryRouter } - ); - - expect(screen.getByTestId('viewer-container')).toBeInTheDocument(); - }); - - it('Should apply customHTMLRenderer to Viewer', async () => { - const markdownWithCode = '```javascript\nconst foo = "bar";\n```'; - const { container } = render( - , - { wrapper: MemoryRouter } - ); - - const markdownParser = await findByTestId(container, 'markdown-parser'); - - expect(markdownParser.querySelector('pre')).toBeInTheDocument(); - }); - - it('Should update readMore state when isDescriptionExpanded changes', () => { - const { rerender } = render( - , - { wrapper: MemoryRouter } - ); - - expect(screen.getByTestId('read-more-button')).toBeInTheDocument(); - - rerender(); - - expect(screen.getByTestId('read-less-button')).toBeInTheDocument(); - }); - - it('Should render with empty markdown string', () => { - render(, { - wrapper: MemoryRouter, - }); - - expect(screen.getByTestId('viewer-container')).toBeInTheDocument(); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewer.tsx deleted file mode 100644 index b161abe7848d..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewer.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2022 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 { Viewer } from '@toast-ui/react-editor'; -import { Button } from 'antd'; -import classNames from 'classnames'; -import { uniqueId } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { DESCRIPTION_MAX_PREVIEW_CHARACTERS } from '../../../constants/constants'; -import { formatContent, isHTMLString } from '../../../utils/BlockEditorUtils'; -import { getTrimmedContent } from '../../../utils/CommonUtils'; -import { - customHTMLRenderer, - replaceLatex, -} from './CustomHtmlRederer/CustomHtmlRederer'; -import './rich-text-editor-previewer.less'; -import { PreviewerProp } from './RichTextEditor.interface'; - -/** - * @deprecated This component is deprecated and will be removed in future releases. - * Please use {@link https://github.com/open-metadata/OpenMetadata/blob/main/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/RichTextEditorPreviewerV1.tsx|RichTextEditorPreviewerV1} instead of this component. - */ -const RichTextEditorPreviewer = ({ - markdown = '', - className = '', - enableSeeMoreVariant = true, - textVariant = 'black', - showReadMoreBtn = true, - maxLength = DESCRIPTION_MAX_PREVIEW_CHARACTERS, - isDescriptionExpanded = false, - reducePreviewLineClass, -}: PreviewerProp) => { - const { t, i18n } = useTranslation(); - const [content, setContent] = useState(''); - // initially read more will be false - const [readMore, setReadMore] = useState(false); - - // read more toggle handler - const handleReadMoreToggle = () => setReadMore((pre) => !pre); - - // whether has read more content or not - const hasReadMore = useMemo( - () => enableSeeMoreVariant && markdown.length > maxLength, - [enableSeeMoreVariant, markdown, maxLength] - ); - - /** - * if hasReadMore is true then value will be based on read more state - * else value will be content - */ - const viewerValue = useMemo(() => { - if (hasReadMore) { - return readMore ? content : `${getTrimmedContent(content, maxLength)}...`; - } - - return content; - }, [hasReadMore, readMore, maxLength, content]); - - useEffect(() => { - setContent( - isHTMLString(markdown) ? formatContent(markdown, 'client') : markdown - ); - }, [markdown]); - - const handleMouseDownEvent = useCallback(async (e: MouseEvent) => { - const targetNode = e.target as HTMLElement; - const previousSibling = targetNode.previousElementSibling as HTMLElement; - const targetNodeDataTestId = targetNode.getAttribute('data-testid'); - - if (targetNodeDataTestId === 'code-block-copy-icon' && previousSibling) { - const codeNode = previousSibling.previousElementSibling; - - const content = codeNode?.textContent ?? ''; - - try { - await navigator.clipboard.writeText(content); - previousSibling.setAttribute('data-copied', 'true'); - targetNode.setAttribute('data-copied', 'true'); - setTimeout(() => { - previousSibling.setAttribute('data-copied', 'false'); - targetNode.setAttribute('data-copied', 'false'); - }, 2000); - } catch (error) { - // handle error - } - } - }, []); - - useEffect(() => { - window.addEventListener('mousedown', handleMouseDownEvent); - - return () => window.removeEventListener('mousedown', handleMouseDownEvent); - }, [handleMouseDownEvent]); - - useEffect(() => { - setReadMore(Boolean(isDescriptionExpanded)); - }, [isDescriptionExpanded]); - - return ( -
-
- -
- {hasReadMore && showReadMoreBtn && ( - - )} -
- ); -}; - -export default RichTextEditorPreviewer; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/rich-text-editor-previewer.less b/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/rich-text-editor-previewer.less deleted file mode 100644 index 86f4a9d4b128..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/RichTextEditor/rich-text-editor-previewer.less +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright 2022 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 './../../../styles/variables.less'; -@border-radius: 6px; -@border-color: #e8e8ed; -@markdown-bg-color: #f8f8fa; -@admonition-border-color: #afafc1; - -.markdown-parser { - .toastui-editor-contents { - font-size: 14px; - h1, - h2, - h3, - h4, - h5, - h6 { - margin: 0px !important; - border: none; - padding: 24px 0px 0px 0px; - color: @text-color; - } - - h1 { - font-size: 24px; - line-height: 48px; - font-weight: 600; - } - - h2 { - font-size: 20px; - line-height: 32px; - font-weight: 600; - } - - h3 { - font-size: 18px; - font-weight: 600; - } - - h4 { - font-size: 16px; - font-weight: 600; - } - - h5 { - font-size: 14px; - font-weight: 600; - } - - section[data-highlighted='true'] { - background: @primary-50; - transition: ease-in-out; - border-left: 3px solid @primary-6; - padding-bottom: 12px !important; - margin-top: 12px; - - h1, - h2, - h3, - h4, - h5 { - padding: 8px 0 0 !important; - } - } - - a { - color: @primary-color; - text-decoration: none; - line-height: 24px; - - &:hover { - text-decoration: underline; - } - } - - table { - border-collapse: collapse; - margin-left: 35px; - } - - th, - td { - border: 1px solid @border-color; - margin: 0; - padding: 8px 16px; - } - - th { - background: @markdown-bg-color; - color: @text-color; - font-weight: 500; - } - - blockquote { - border-left: 4px solid @border-color; - padding-left: 16px; - margin-left: 25px; - } - - strong { - font-weight: bold; - } - - code { - white-space: break-spaces; - background: @markdown-bg-color; - color: @text-color; - } - - pre { - margin: 0px; - padding: 0px; - background: transparent; - margin-top: 16px; - code { - display: block; - padding: 15px; - overflow: auto; - border-radius: 8px; - border-bottom: 2px solid @border-color; - } - } - - img { - border-radius: @border-radius; - margin-top: 10px; - max-width: 100%; - } - - li:not(:last-child) { - margin-bottom: 5px; - } - - p { - margin-top: 10px; - margin-bottom: 0px; - color: @text-color; - word-break: break-word; - line-height: 20px; - &:first-child { - margin-top: 0px; - } - &:last-child { - margin-bottom: 0px; - } - } - - ul { - list-style: disc !important; - li { - &::before { - content: none; - } - } - } - - .task-list-item { - border-radius: 0.125rem; - border-width: 1px; - border-color: @text-color; - } - .task-list-item.checked { - background-size: 100% 100%; - background-position: center; - background-repeat: no-repeat; - &::before { - background-image: url('data:image/svg+xml,%3csvg viewBox=%270 0 16 16%27 fill=%27white%27 xmlns=%27http://www.w3.org/2000/svg%27%3e%3cpath d=%27M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z%27/%3e%3c/svg%3e'); - background-color: @primary-color; - } - } - .admonition { - border-radius: 8px; - border-left: 8px solid @admonition-border-color; - padding: 1px 16px 1px 48px !important; - margin: 16px; - background-color: @markdown-bg-color; - position: relative; - - &::before { - content: 'ℹ️'; - position: absolute; - top: 16px; - left: 16px; - } - p { - margin-top: 16px; - margin-bottom: 16px; - } - } - .admonition_note { - &::before { - content: '📝'; - } - } - .admonition_warning, - .admonition_caution { - background-color: #fff3dc; - border-left-color: @warning-color; - &::before { - content: '⚠️'; - } - } - .admonition_tip, - .admonition_info { - background-color: @primary-1; - border-left-color: @info-color; - } - .admonition_tip { - &::before { - content: '💡'; - } - } - .admonition_danger { - background-color: #ff4c3b33; - border-left-color: @error-color; - - &::before { - content: '⚠️'; - } - } - .code-copy-button { - display: none; - position: absolute; - top: 8px; - right: 40px; - pointer-events: all; - cursor: pointer; - z-index: 1; - padding: 2px; - background: @border-color; - border-radius: 4px; - } - - .code-copy-message { - display: none; - position: absolute; - top: 16px; - right: 40px; - pointer-events: all; - cursor: pointer; - z-index: 2; - background: @border-color; - border-radius: 4px; - padding: 0px 4px; - } - - .code-block { - &:hover .code-copy-button[data-copied='false'] { - display: inline-block; - } - } - - .code-copy-message[data-copied='true'] { - display: inline-block; - } - .code-copy-message[data-copied='false'] { - display: none; - } - .code-copy-button[data-copied='true'] { - opacity: 0; - } - // for latex expressions - .katex { - mtext { - font-size: 14px; - } - } - } -} - -.markdown-parser.white { - .toastui-editor-contents { - p { - color: @white; - } - ul li::before { - background-color: @white; - } - } -} - -.rich-text-editor-container { - ol, - ul { - margin: unset; - padding-inline-start: 20px; - } - .ant-btn { - padding: 0px; - margin: 0px; - line-height: 0px; - height: 16px; - } - - .ant-btn:focus, - .ant-btn:hover { - color: @primary-color; - } -} - -.rich-text-editor-container.text-grey-muted { - .markdown-parser { - .toastui-editor-contents { - p { - color: @grey-4; - } - ul li::before { - background-color: @grey-4; - } - } - } -} 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 86c2c39b5487..89798ce93ebc 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 @@ -11,22 +11,61 @@ * limitations under the License. */ import { Col, Row } from 'antd'; +import DOMPurify from 'dompurify'; import { first, last, noop } from 'lodash'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ENDS_WITH_NUMBER_REGEX, + MARKDOWN_MATCH_ID, ONEOF_ANYOF_ALLOF_REGEX, } from '../../../constants/regex.constants'; import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { fetchMarkdownFile } from '../../../rest/miscAPI'; import { SupportedLocales } from '../../../utils/i18next/LocalUtil.interface'; +import { MarkdownToHTMLConverter } from '../../../utils/FeedUtils'; import { getActiveFieldNameForAppDocs } from '../../../utils/ServiceUtils'; import EntitySummaryPanel from '../../Explore/EntitySummaryPanel/EntitySummaryPanel.component'; import { SearchedDataProps } from '../../SearchedData/SearchedData.interface'; import Loader from '../Loader/Loader'; -import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer'; import './service-doc-panel.less'; + +const SECTION_BLOCK_REGEX = /\$\$section\n([\s\S]*?)\n\$\$/g; + +const processServiceDocMarkdown = (markdown: string): string => { + const parts: string[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + + SECTION_BLOCK_REGEX.lastIndex = 0; + + while ((match = SECTION_BLOCK_REGEX.exec(markdown)) !== null) { + if (match.index > lastIndex) { + parts.push( + MarkdownToHTMLConverter.makeHtml(markdown.slice(lastIndex, match.index)) + ); + } + + const sectionContent = match[1]; + const idMatch = sectionContent.match(MARKDOWN_MATCH_ID); + const id = idMatch ? idMatch[1] : ''; + const cleanContent = sectionContent.replace(MARKDOWN_MATCH_ID, '').trim(); + + parts.push( + `
${MarkdownToHTMLConverter.makeHtml( + cleanContent + )}
` + ); + + lastIndex = match.index + match[0].length; + } + + if (lastIndex < markdown.length) { + parts.push(MarkdownToHTMLConverter.makeHtml(markdown.slice(lastIndex))); + } + + return parts.join('\n'); +}; interface ServiceDocPanelProp { serviceName: string; serviceType: string; @@ -162,6 +201,14 @@ const ServiceDocPanel: FC = ({ } }, [activeField, serviceType, isMarkdownReady]); + const processedHtml = useMemo( + () => + DOMPurify.sanitize(processServiceDocMarkdown(markdownContent), { + ADD_ATTR: ['data-id', 'data-highlighted', 'target'], + }), + [markdownContent] + ); + const docsPanel = useMemo(() => { return ( <> @@ -176,13 +223,13 @@ const ServiceDocPanel: FC = ({ )}
- ); - }, [markdownContent, serviceName, selectedEntity]); + }, [processedHtml, serviceName, selectedEntity]); if (isLoading) { return ; 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 a2b11fcf157c..cdca4e5e9ee7 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 @@ -12,16 +12,126 @@ */ @import '../../../styles/variables.less'; +@markdown-bg-color: #f8f8fa; + .service-doc-panel { - .toastui-editor-contents { + .service-doc-content { + font-size: 14px; + padding: 4px 24px; + + h1, + h2, + h3, + h4, + h5, + h6 { + margin: 0 !important; + border: none; + padding: 16px 0 4px; + color: @text-color; + } + h1 { - &:first-child { - padding-top: 8px !important; + font-size: 24px; + line-height: 48px; + font-weight: 600; + } + + h2 { + font-size: 20px; + line-height: 32px; + font-weight: 600; + } + + h3 { + font-size: 18px; + font-weight: 600; + } + + h4 { + font-size: 16px; + font-weight: 600; + } + + h5 { + font-size: 14px; + font-weight: 600; + } + + p { + margin: 4px 0; + line-height: 24px; + } + + a { + color: @primary-color; + text-decoration: none; + + &:hover { + text-decoration: underline; } } - & > * { - padding: 4px 24px !important; + ul, + ol { + margin: 4px 0; + padding-inline-start: 20px; + } + + strong { + font-weight: bold; + } + + code { + white-space: break-spaces; + background: @markdown-bg-color; + color: @text-color; + padding: 1px 4px; + border-radius: 4px; + } + + pre { + margin: 8px 0; + padding: 0; + background: transparent; + + code { + display: block; + padding: 15px; + overflow: auto; + border-radius: 8px; + border-bottom: 2px solid @border-color; + background: @markdown-bg-color; + } + } + + blockquote { + border-left: 4px solid @border-color; + margin: 4px 0; + padding-left: 12px; + } + + table { + border-collapse: collapse; + width: 100%; + margin: 8px 0; + } + + th, + td { + border: 1px solid @border-color; + padding: 8px 16px; + } + + th { + background: @markdown-bg-color; + font-weight: 500; + } + + section[data-highlighted='true'] { + background-color: @primary-50; + border-left: 4px solid @primary-color; + padding-left: 12px; } } diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index e46a96918b9c..dbecf0ff37fc 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -2421,9 +2421,8 @@ compare-versions "^4.1.2" "@openmetadata/ui-core-components@link:../../../../../openmetadata-ui-core-components/src/main/resources/ui": - version "1.0.0" - dependencies: - "@material/material-color-utilities" "^0.3.0" + version "0.0.0" + uid "" "@peculiar/asn1-schema@^2.3.13", "@peculiar/asn1-schema@^2.3.8": version "2.6.0" @@ -5053,27 +5052,6 @@ resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.27.2.tgz#901c1bbb5f12002cfe78a1ad40577727c23c374e" integrity sha512-dQyvCIg0hcAVeh4fCIVCxogvbp+bF+GpbUb8sNlgnGrmHXnapGxzkvrlHnvneXZxLk/j7CxmBPKJNnm4Pbx4zw== -"@toast-ui/editor@^3.2.2": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@toast-ui/editor/-/editor-3.2.2.tgz#1f2837271c5c9c3e29e090d7440bfc6ab23fb4c4" - integrity sha512-ASX7LFjN2ZYQJrwmkUajPs7DRr9FsM1+RQ82CfTO0Y5ZXorBk1VZS4C2Dpxinx9kl55V4F8/A2h2QF4QMDtRbA== - dependencies: - dompurify "^2.3.3" - prosemirror-commands "^1.1.9" - prosemirror-history "^1.1.3" - prosemirror-inputrules "^1.1.3" - prosemirror-keymap "^1.1.4" - prosemirror-model "^1.14.1" - prosemirror-state "^1.3.4" - prosemirror-view "^1.18.7" - -"@toast-ui/react-editor@^3.1.8": - version "3.2.3" - resolved "https://registry.yarnpkg.com/@toast-ui/react-editor/-/react-editor-3.2.3.tgz#e335f99595709b5b1961d3d77a0338a07c419a32" - integrity sha512-86QdgiOkBeSwRBEUWRKsTpnm6yu5j9HNJ3EfQN8EGcd7kI8k8AhExXyUJ3NNgNTzN7FfSKMw+1VaCDDC+aZ3dw== - dependencies: - "@toast-ui/editor" "^3.2.2" - "@tootallnate/once@2", "@tootallnate/once@3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-3.0.1.tgz#d580decb59cb41a15856387a86800838102daf44" @@ -7666,7 +7644,7 @@ domhandler@4.3.1, domhandler@^4.2.0, domhandler@^4.2.2: dependencies: domelementtype "^2.2.0" -dompurify@3.3.2, dompurify@^2.3.3: +dompurify@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.2.tgz#58c515d0f8508b8749452a028aa589ad80b36325" integrity sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ== @@ -11319,7 +11297,7 @@ prosemirror-collab@^1.3.1: dependencies: prosemirror-state "^1.0.0" -prosemirror-commands@^1.0.0, prosemirror-commands@^1.1.9, prosemirror-commands@^1.6.2: +prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2: version "1.7.1" resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz#d101fef85618b1be53d5b99ea17bee5600781b38" integrity sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w== @@ -11347,7 +11325,7 @@ prosemirror-gapcursor@^1.3.2: prosemirror-state "^1.0.0" prosemirror-view "^1.0.0" -prosemirror-history@^1.0.0, prosemirror-history@^1.1.3, prosemirror-history@^1.4.1: +prosemirror-history@^1.0.0, prosemirror-history@^1.4.1: version "1.5.0" resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.5.0.tgz#ee21fc5de85a1473e3e3752015ffd6d649a06859" integrity sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg== @@ -11357,7 +11335,7 @@ prosemirror-history@^1.0.0, prosemirror-history@^1.1.3, prosemirror-history@^1.4 prosemirror-view "^1.31.0" rope-sequence "^1.3.0" -prosemirror-inputrules@^1.1.3, prosemirror-inputrules@^1.4.0: +prosemirror-inputrules@^1.4.0: version "1.5.1" resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz#d2e935f6086e3801486b09222638f61dae89a570" integrity sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw== @@ -11365,7 +11343,7 @@ prosemirror-inputrules@^1.1.3, prosemirror-inputrules@^1.4.0: prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.4, prosemirror-keymap@^1.2.2, prosemirror-keymap@^1.2.3: +prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2, prosemirror-keymap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz#c0f6ab95f75c0b82c97e44eb6aaf29cbfc150472" integrity sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw== @@ -11392,7 +11370,7 @@ prosemirror-menu@^1.2.4: prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@1.22.1, prosemirror-model@^1.0.0, prosemirror-model@^1.14.1, prosemirror-model@^1.16.0, prosemirror-model@^1.23.0, prosemirror-model@^1.25.0, prosemirror-model@^1.25.4: +prosemirror-model@1.22.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.23.0, prosemirror-model@^1.25.0, prosemirror-model@^1.25.4: version "1.22.1" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.22.1.tgz#2ed7d7840e710172c559d5a9950e92b870d1e764" integrity sha512-gMrxal+F3higDFxCkBK5iQXckRVYvIu/3dopERJ6b20xfwZ9cbYvQvuldqaN+v/XytNPGyURYUpUU23kBRxWCQ== @@ -11415,7 +11393,7 @@ prosemirror-schema-list@^1.4.1: prosemirror-state "^1.0.0" prosemirror-transform "^1.7.3" -prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.4, prosemirror-state@^1.4.3, prosemirror-state@^1.4.4: +prosemirror-state@1.4.1, prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3, prosemirror-state@^1.4.4: version "1.4.1" resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.1.tgz#f6e26c7b6a7e11206176689eb6ebbf91870953e1" integrity sha512-U/LBDW2gNmVa07sz/D229XigSdDQ5CLFwVB1Vb32MJbAHHhWe/6pOc721faI17tqw4pZ49i1xfY/jEZ9tbIhPg== @@ -11449,7 +11427,7 @@ prosemirror-transform@1.7.0, prosemirror-transform@^1.0.0, prosemirror-transform dependencies: prosemirror-model "^1.0.0" -prosemirror-view@1.28.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.18.7, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.41.4: +prosemirror-view@1.28.2, prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.41.4: version "1.28.2" resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.28.2.tgz#e997ef9dc623d01afd170e328fc924e6f4382003" integrity sha512-uK28mJbu0GI8Oz7Aclt6BKL4g+C59EBShBXDB0Y9Y71H25p4bQgmLQLfDWjsT1J9XOw0bR8QQajZmdK8RvXI9g== From 64f0d7dfc38541834bd2bdddd1e6f1788fef12d0 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Sat, 4 Apr 2026 21:08:07 +0530 Subject: [PATCH 02/12] fix checkstyle & unit tests --- .../ResourcePlayer/ArticleViewer.test.tsx | 2 +- .../ServiceDocPanel/ServiceDocPanel.test.tsx | 27 +++++++++---------- .../ServiceDocPanel/ServiceDocPanel.tsx | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.test.tsx index 0bfe4531c6d4..cd6481934131 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Learning/ResourcePlayer/ArticleViewer.test.tsx @@ -19,7 +19,7 @@ jest.mock('../../../utils/ToastUtils', () => ({ showErrorToast: jest.fn(), })); -jest.mock('../../common/RichTextEditor/RichTextEditorPreviewer', () => { +jest.mock('../../common/RichTextEditor/RichTextEditorPreviewerV1', () => { return jest .fn() .mockImplementation(({ markdown }) => ( 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 a2f152bd4f69..c11f16a9385c 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 @@ -20,14 +20,6 @@ jest.mock('../Loader/Loader', () => jest.fn().mockReturnValue(
Loader
) ); -jest.mock('../RichTextEditor/RichTextEditorPreviewer', () => - jest - .fn() - .mockImplementation(({ markdown }) => ( -
{markdown}
- )) -); - jest.mock('../../Explore/EntitySummaryPanel/EntitySummaryPanel.component', () => jest .fn() @@ -113,11 +105,13 @@ describe('ServiceDocPanel Component', () => { describe('Core Functionality', () => { it('should render component and fetch markdown content', async () => { - render(); + const { container } = render(); await waitFor(() => { expect(screen.getByTestId('service-requirements')).toBeInTheDocument(); - expect(screen.getByTestId('requirement-text')).toBeInTheDocument(); + expect( + container.querySelector('.service-doc-content') + ).not.toBeNull(); expect(mockFetchMarkdownFile).toHaveBeenCalledWith( 'en-US/DatabaseService/mysql.md' ); @@ -168,10 +162,13 @@ describe('ServiceDocPanel Component', () => { it('should handle fetch failures gracefully', async () => { mockFetchMarkdownFile.mockRejectedValue(new Error('Network error')); - render(); + const { container } = render(); await waitFor(() => { - expect(screen.getByTestId('requirement-text')).toHaveTextContent(''); + const docContent = container.querySelector('.service-doc-content'); + + expect(docContent).not.toBeNull(); + expect(docContent?.innerHTML).toBe(''); }); }); }); @@ -281,7 +278,7 @@ describe('ServiceDocPanel Component', () => { mockQuerySelector.mockReturnValue(mockElement); mockGetActiveFieldNameForAppDocs.mockReturnValue('application.config'); - render( + const { container } = render( { await waitFor(() => { expect(screen.getByTestId('entity-summary-panel')).toBeInTheDocument(); - expect(screen.getByTestId('requirement-text')).toBeInTheDocument(); + expect( + container.querySelector('.service-doc-content') + ).not.toBeNull(); expect(mockGetActiveFieldNameForAppDocs).toHaveBeenCalledWith( 'root/application/config' ); 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 89798ce93ebc..7e6f553cdd57 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 @@ -22,8 +22,8 @@ import { } from '../../../constants/regex.constants'; import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { fetchMarkdownFile } from '../../../rest/miscAPI'; -import { SupportedLocales } from '../../../utils/i18next/LocalUtil.interface'; import { MarkdownToHTMLConverter } from '../../../utils/FeedUtils'; +import { SupportedLocales } from '../../../utils/i18next/LocalUtil.interface'; import { getActiveFieldNameForAppDocs } from '../../../utils/ServiceUtils'; import EntitySummaryPanel from '../../Explore/EntitySummaryPanel/EntitySummaryPanel.component'; import { SearchedDataProps } from '../../SearchedData/SearchedData.interface'; From 77c351b88d244390a3d1a893e019bf2f5eb0f44b Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Sun, 5 Apr 2026 12:58:24 +0530 Subject: [PATCH 03/12] fix checkstyle --- .../common/ServiceDocPanel/ServiceDocPanel.test.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 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 c11f16a9385c..b2a1ece894c0 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 @@ -109,9 +109,7 @@ describe('ServiceDocPanel Component', () => { await waitFor(() => { expect(screen.getByTestId('service-requirements')).toBeInTheDocument(); - expect( - container.querySelector('.service-doc-content') - ).not.toBeNull(); + expect(container.querySelector('.service-doc-content')).not.toBeNull(); expect(mockFetchMarkdownFile).toHaveBeenCalledWith( 'en-US/DatabaseService/mysql.md' ); @@ -289,9 +287,7 @@ describe('ServiceDocPanel Component', () => { await waitFor(() => { expect(screen.getByTestId('entity-summary-panel')).toBeInTheDocument(); - expect( - container.querySelector('.service-doc-content') - ).not.toBeNull(); + expect(container.querySelector('.service-doc-content')).not.toBeNull(); expect(mockGetActiveFieldNameForAppDocs).toHaveBeenCalledWith( 'root/application/config' ); From 2508aa5ea8ca9a95802cc503d55ca29d9fee0cbb Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 13 Apr 2026 23:27:34 +0530 Subject: [PATCH 04/12] Fixed SSODoc Panel bugs --- .../locales/en-US/SSO/auth0SSOClientConfig.md | 124 +++++--- .../public/locales/en-US/SSO/aws-cognito.md | 101 +++++-- .../en-US/SSO/awsCognitoSSOClientConfig.md | 120 +++++--- .../locales/en-US/SSO/azureSSOClientConfig.md | 125 +++++--- .../en-US/SSO/customOidcSSOClientConfig.md | 63 ++-- .../en-US/SSO/googleSSOClientConfig.md | 129 +++++--- .../locales/en-US/SSO/ldapSSOClientConfig.md | 149 +++++++--- .../locales/en-US/SSO/oktaSSOClientConfig.md | 124 +++++--- .../locales/en-US/SSO/samlSSOClientConfig.md | 85 ++++-- .../SSOConfigurationForm.tsx | 8 +- .../sso-configuration-form.less | 5 - .../SSODocPanel/SSODocPanel.constants.ts | 1 + .../SettingsSso/SSODocPanel/SSODocPanel.tsx | 275 ++++-------------- .../SSODocPanel/sso-doc-panel.less | 244 +--------------- .../ServiceDocPanel/ServiceDocPanel.tsx | 46 +-- .../ServiceDocPanel/service-doc-panel.less | 1 + .../resources/ui/src/utils/ServiceUtils.tsx | 43 +++ 17 files changed, 839 insertions(+), 804 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/auth0SSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/auth0SSOClientConfig.md index e7d15bc8bdb8..bb2c254bf48c 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/auth0SSOClientConfig.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/auth0SSOClientConfig.md @@ -1,8 +1,3 @@ ---- -title: Auth0 SSO Configuration | OpenMetadata -description: Configure Auth0 Active Directory Single Sign-On for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/azure-ad-sso ---- # Auth0 SSO Configuration @@ -10,14 +5,17 @@ Auth0 Active Directory (Auth0) SSO enables users to log in with their Auth0 acco ## Authentication Configuration -### Provider Name +$$section +### Provider Name $(id="providerName") - **Definition:** A human-readable name for this Auth0 SSO configuration instance. - **Example:** Auth0 SSO, Company Auth0, Custom Identity Provider - **Why it matters:** Helps identify this specific SSO configuration in logs and user interfaces. - **Note:** This is a display name and doesn't affect authentication functionality. +$$ -### Client Type +$$section +### Client Type $(id="clientType") - **Definition:** Defines whether the application is public (no client secret) or confidential (requires client secret). - **Options:** Public | Confidential @@ -27,23 +25,29 @@ Auth0 Active Directory (Auth0) SSO enables users to log in with their Auth0 acco - Choose **Public** for SPAs and mobile apps - Choose **Confidential** for backend services and web applications - Auth0 typically uses **Confidential** client type +$$ -### Enable Self Signup +$$section +### Enable Self Signup $(id="selfSignup") - **Definition:** Allows users to automatically create accounts on first login. - **Options:** Enabled | Disabled - **Example:** Enabled - **Why it matters:** Controls whether new users can join automatically or need manual approval. - **Note:** Disable for stricter control over user access. +$$ -### Client ID +$$section +### Client ID $(id="clientId") - **Definition:** Application (client) ID assigned to your app in Auth0. - **Example:** abc123def456ghi789jkl012mno345pqr - **Why it matters:** Auth0 uses this to identify your application during authentication. - **Note:** Found in Auth0 → Applications → Your app → Overview → Application (client) ID +$$ -### Callback URL +$$section +### Callback URL $(id="callbackUrl") - **Definition:** Redirect URI where Auth0 sends authentication responses. - **Example:** https://yourapp.company.com/callback @@ -51,8 +55,10 @@ Auth0 Active Directory (Auth0) SSO enables users to log in with their Auth0 acco - **Note:** - Must be registered in Auth0 → Applications → Authentication → Redirect URIs - Always use HTTPS in production +$$ -### Authority +$$section +### Authority $(id="authority") - **Definition:** Auth0 endpoint that issues tokens for your tenant. - **Example:** https://dev-abc123.us.auth0.com/your-auth0-domain @@ -60,15 +66,19 @@ Auth0 Active Directory (Auth0) SSO enables users to log in with their Auth0 acco - **Note:** - Replace `your-auth0-domain` with your actual Auth0 tenant ID - For multi-tenant apps, you can use `common` instead of tenant ID +$$ -### Public Key URLs +$$section +### Public Key URLs $(id="publicKey") - **Definition:** List of URLs where Auth0 publishes its public keys for token verification. - **Example:** ["https://dev-abc123.us.auth0.com/common/discovery/v2.0/keys"] - **Why it matters:** Used to verify JWT token signatures from Auth0. - **Note:** Usually auto-discovered from the discovery URI, rarely needs manual configuration +$$ -### JWT Principal Claims +$$section +### JWT Principal Claims $(id="principals") > ⚠️ **CRITICAL WARNING**: Incorrect claims will **lock out ALL users including admins**! > - These claims MUST exist in JWT tokens from Auth0 @@ -82,8 +92,10 @@ Auth0 Active Directory (Auth0) SSO enables users to log in with their Auth0 acco - **Why it matters:** Determines which claim from the JWT token identifies the user. - **Note:** Common Auth0 claims: email, name, sub, nickname - Order matters; first matching claim is used +$$ -### JWT Principal Claims Mapping +$$section +### JWT Principal Claims Mapping $(id="jwtPrincipalClaimsMapping") - **Definition:** Maps JWT claims to OpenMetadata user attributes. (Overrides jwtPrincipalClaims if set) - **Example:** ["email:email", "username:preferred_username"] @@ -94,8 +106,10 @@ Auth0 Active Directory (Auth0) SSO enables users to log in with their Auth0 acco - Only `username` and `email` keys are allowed; no other keys are permitted - If validation fails, errors will be displayed on this specific field - **Important:** JWT Principal Claims Mapping is **rarely needed** for most Auth0 configurations. The default JWT Principal Claims (`email`, `name`, `sub`) handle user identification correctly. Only configure this if you have specific custom claim requirements. +$$ -### JWT Team Claim Mapping +$$section +### JWT Team Claim Mapping $(id="jwtTeamClaimMapping") - **Definition:** Auth0 claim or attribute containing team/department information for automatic team assignment. - **Example:** "department", "groups", "organization", or custom user metadata fields @@ -121,15 +135,19 @@ Auth0 Active Directory (Auth0) SSO enables users to log in with their Auth0 acco ## OIDC Configuration (Confidential Client Only) These fields are only shown when Client Type is set to **Confidential**. +$$ -### OIDC Client ID +$$section +### OIDC Client ID $(id="id") - **Definition:** Application (client) ID for OIDC authentication with Auth0. - **Example:** abc123def456ghi789jkl012mno345pqr - **Why it matters:** Identifies your application to Auth0 in OIDC flows. - **Note:** Same as the Client ID in Auth0 app registration +$$ -### OIDC Client Secret +$$section +### OIDC Client Secret $(id="clientSecret") - **Definition:** Secret key for confidential client authentication with Auth0. - **Example:** abc123def456ghi789jkl012mno345pqr678st @@ -138,70 +156,85 @@ These fields are only shown when Client Type is set to **Confidential**. - Generate in Auth0 → Applications → Certificates & secrets - Store securely and rotate regularly - Only shown for Confidential client type +$$ -### OIDC Request Scopes +$$section +### OIDC Request Scopes $(id="scopes") - **Definition:** Permissions requested from Auth0 during authentication. - **Default:** openid email profile - **Example:** openid email profile User.Read - **Why it matters:** Determines what user information OpenMetadata can access. - **Note:** `openid email profile` are typically sufficient for most use cases +$$ -### OIDC Discovery URI +$$section +### OIDC Discovery URI $(id="discoveryUri") - **Definition:** Auth0's OpenID Connect metadata endpoint. - **Example:** https://dev-abc123.us.auth0.com/your-auth0-domain/v2.0/.well-known/openid-configuration - **Why it matters:** Allows OpenMetadata to automatically discover Auth0's OIDC endpoints. - **Note:** Replace `your-auth0-domain` with your actual tenant ID +$$ -### OIDC Use Nonce +$$section +### OIDC Use Nonce $(id="useNonce") - **Definition:** Security feature to prevent replay attacks in OIDC flows. - **Default:** false - **Example:** false - **Why it matters:** Enhances security by ensuring each authentication request is unique. - **Note:** Can be enabled for additional security if your provider supports it +$$ - - -### OIDC Disable PKCE +$$section +### OIDC Disable PKCE $(id="disablePkce") - **Definition:** Whether to disable Proof Key for Code Exchange (security extension). - **Default:** false - **Example:** false - **Why it matters:** PKCE adds security to the authorization code flow. - **Note:** Should typically be left enabled (false) for security +$$ -### OIDC Max Clock Skew +$$section +### OIDC Max Clock Skew $(id="maxClockSkew") - **Definition:** Maximum allowed time difference between systems when validating tokens. - **Example:** 0 (seconds) - **Why it matters:** Prevents token validation failures due to minor time differences. - **Note:** Usually 0 is fine unless you have significant clock skew issues +$$ -### OIDC Client Authentication Method +$$section +### OIDC Client Authentication Method $(id="clientAuthenticationMethod") - **Definition:** Method used to authenticate the client with Auth0. - **Default:** client_secret_post (automatically configured) - **Why it matters:** OpenMetadata uses `client_secret_post` which is supported by Auth0. - **Note:** This field is hidden and automatically configured. Auth0 supports both `client_secret_post` and `client_secret_basic`. +$$ -### OIDC Token Validity +$$section +### OIDC Token Validity $(id="tokenValidity") - **Definition:** How long (in seconds) the issued tokens remain valid. - **Default:** 0 (use provider default) - **Example:** 3600 (1 hour) - **Why it matters:** Controls token lifetime and security vs usability balance. +$$ -### OIDC Custom Parameters +$$section +### OIDC Custom Parameters $(id="customParams") - **Definition:** Additional parameters to send in OIDC requests. - **Example:** {"prompt": "select_account", "domain_hint": "company.com"} - **Why it matters:** Allows customization of Auth0 authentication behavior. - **Note:** Common parameters include `prompt`, `domain_hint`, `login_hint` +$$ - -### OIDC Callback URL / Redirect URI +$$section +### OIDC Callback URL / Redirect URI $(id="callbackUrl") - **Definition:** URL where Auth0 redirects after authentication. - **Auto-Generated:** This field is automatically populated as `{your-domain}/callback`. @@ -211,15 +244,19 @@ These fields are only shown when Client Type is set to **Confidential**. - **This field is read-only** - it cannot be edited - **Copy this exact URL** and add it to Auth0's allowed redirect URIs list - Format is always: `{your-domain}/callback` +$$ -### OIDC Max Age +$$section +### OIDC Max Age $(id="maxAge") - **Definition:** Maximum authentication age (in seconds) before re-authentication is required. - **Example:** 3600 - **Why it matters:** Controls how often users must re-authenticate. - **Note:** Leave empty for no specific max age requirement +$$ -### OIDC Prompt +$$section +### OIDC Prompt $(id="prompt") - **Definition:** Controls Auth0's authentication prompts. - **Options:** none | login | consent | select_account @@ -229,8 +266,10 @@ These fields are only shown when Client Type is set to **Confidential**. - `login`: Always prompt for credentials - `consent`: Prompt for permissions - `select_account`: Show account picker +$$ -### OIDC Session Expiry +$$section +### OIDC Session Expiry $(id="sessionExpiry") - **Definition:** How long (in seconds) user sessions remain valid. - **Default:** 604800 (7 days) @@ -239,29 +278,37 @@ These fields are only shown when Client Type is set to **Confidential**. - **Note:** Only applies to confidential clients ## Authorizer Configuration +$$ -### Admin Principals +$$section +### Admin Principals $(id="adminPrincipals") - **Definition:** List of user principals who will have admin access. - **Example:** ["admin", "superuser"] - **Why it matters:** These users will have full administrative privileges in OpenMetadata. - **Note:** Use usernames (NOT email addresses) - these are derived from the email prefix (part before @) +$$ -### Principal Domain +$$section +### Principal Domain $(id="principalDomain") - **Definition:** Default domain for user principals. - **Example:** company.com - **Why it matters:** Used to construct full user principals when only username is provided. - **Note:** Typically your organization's primary domain +$$ -### Enforce Principal Domain +$$section +### Enforce Principal Domain $(id="enforcePrincipalDomain") - **Definition:** Whether to enforce that all users belong to the principal domain. - **Default:** false - **Example:** true - **Why it matters:** Adds an extra layer of security by restricting access to users from specific domains. +$$ -### Allowed Domains +$$section +### Allowed Domains $(id="allowedDomains") - **Definition:** List of email domains that are permitted to access OpenMetadata. - **Example:** ["company.com", "partner-company.com"] @@ -271,11 +318,14 @@ These fields are only shown when Client Type is set to **Confidential**. - When `enforcePrincipalDomain` is enabled, only users with email addresses from these domains can access OpenMetadata - Leave empty or use single `principalDomain` if you only have one Auth0 tenant - Useful when your Auth0 tenant contains users from multiple domains +$$ -### Enable Secure Socket Connection +$$section +### Enable Secure Socket Connection $(id="enableSecureSocketConnection") - **Definition:** Whether to use SSL/TLS for secure connections. - **Default:** false - **Example:** true - **Why it matters:** Ensures encrypted communication for security. - **Note:** Should be enabled in production environments +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/aws-cognito.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/aws-cognito.md index 07ad07258454..330ffed53efa 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/aws-cognito.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/aws-cognito.md @@ -1,14 +1,10 @@ ---- -title: AWS Cognito SSO Configuration | OpenMetadata -description: Configure AWS Cognito Single Sign-On for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/aws-cognito-sso ---- # AWS Cognito SSO Configuration AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credentials using OAuth 2.0 and OpenID Connect (OIDC). -## Client Type +$$section +## Client Type $(id="clientType") - **Definition:** Defines whether the application is public (no client secret) or confidential (requires client secret). - **Options:** Public | Confidential @@ -17,23 +13,29 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - **Note:** - Choose **Public** for SPAs and mobile apps - Choose **Confidential** for backend services and web applications +$$ -## Enable Self Signup +$$section +## Enable Self Signup $(id="enableSelfSignup") - **Definition:** Allows users to automatically create accounts on first login. - **Options:** Enabled | Disabled - **Example:** Enabled - **Why it matters:** Controls whether new users can join automatically or need manual approval. - **Note:** Must also be enabled in your Cognito User Pool settings. +$$ -## OIDC Client ID +$$section +## OIDC Client ID $(id="clientId") - **Definition:** App client ID from your AWS Cognito User Pool. - **Example:** 1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p - **Why it matters:** AWS Cognito uses this to identify your application during authentication. - **Note:** Found in AWS Console → Cognito → User Pools → Your pool → App integration → App clients +$$ -## OIDC Callback URL +$$section +## OIDC Callback URL $(id="callbackUrl") - **Definition:** Redirect URI where AWS Cognito sends authentication responses. - **Example:** https://yourapp.company.com/callback @@ -41,15 +43,19 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - **Note:** - Must be registered in Cognito User Pool → App client → Hosted UI → Allowed callback URLs - Always use HTTPS in production +$$ -## Authority +$$section +## Authority $(id="authority") - **Definition:** AWS Cognito User Pool domain that issues tokens. - **Example:** https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123DEF - **Why it matters:** Tells OpenMetadata which Cognito User Pool to authenticate against. - **Note:** Format: https://cognito-idp.{region}.amazonaws.com/{user-pool-id} +$$ -## OIDC Client Secret +$$section +## OIDC Client Secret $(id="secret") - **Definition:** App client secret for confidential client authentication with AWS Cognito. - **Example:** 1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f3g4h @@ -58,39 +64,49 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - Only shown for Confidential client type - Generate in Cognito → User Pool → App client → Generate client secret - Store securely and rotate regularly +$$ -## OIDC Request Scopes +$$section +## OIDC Request Scopes $(id="scope") - **Definition:** Permissions requested from AWS Cognito during authentication. - **Default:** openid email profile - **Example:** openid email profile aws.cognito.signin.user.admin - **Why it matters:** Determines what user information OpenMetadata can access. - **Note:** Must be configured in your Cognito User Pool app client settings +$$ -## OIDC Discovery URI +$$section +## OIDC Discovery URI $(id="discoveryUri") - **Definition:** AWS Cognito's OpenID Connect metadata endpoint. - **Example:** https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123DEF/.well-known/openid_configuration - **Why it matters:** Allows OpenMetadata to automatically discover Cognito's OIDC endpoints. - **Note:** Replace {region} and {user-pool-id} with your actual values +$$ -## OIDC Use Nonce +$$section +## OIDC Use Nonce $(id="useNonce") - **Definition:** Security feature to prevent replay attacks in OIDC flows. - **Default:** false - **Example:** false - **Why it matters:** Enhances security by ensuring each authentication request is unique. - **Note:** Can be enabled for additional security if your provider supports it +$$ -## OIDC Preferred JWS Algorithm +$$section +## OIDC Preferred JWS Algorithm $(id="preferredJwsAlgorithm") - **Definition:** Algorithm used to verify JWT token signatures from AWS Cognito. - **Default:** RS256 - **Example:** RS256 - **Why it matters:** Must match Cognito's token signing algorithm. - **Note:** AWS Cognito uses RS256, rarely needs to be changed +$$ -## OIDC Response Type +$$section +## OIDC Response Type $(id="responseType") - **Definition:** Type of response expected from AWS Cognito during authentication. - **Default:** id_token @@ -98,59 +114,75 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - **Example:** code - **Why it matters:** Determines the OAuth flow type (implicit vs authorization code). - **Note:** Authorization code flow (code) is more secure and recommended +$$ -## OIDC Disable PKCE +$$section +## OIDC Disable PKCE $(id="disablePkce") - **Definition:** Whether to disable Proof Key for Code Exchange (security extension). - **Default:** false - **Example:** false - **Why it matters:** PKCE adds security to the authorization code flow. - **Note:** Should typically be left enabled (false) for security +$$ -## OIDC Max Clock Skew +$$section +## OIDC Max Clock Skew $(id="maxClockSkew") - **Definition:** Maximum allowed time difference between systems when validating tokens. - **Example:** 0 (seconds) - **Why it matters:** Prevents token validation failures due to minor time differences. - **Note:** Usually 0 is fine unless you have significant clock skew issues +$$ -## OIDC Client Authentication Method +$$section +## OIDC Client Authentication Method $(id="clientAuthenticationMethod") - **Definition:** Method used to authenticate the client with AWS Cognito. - **Default:** client_secret_post (automatically configured) - **Why it matters:** OpenMetadata uses `client_secret_post` which is supported by AWS Cognito. - **Note:** This field is hidden and automatically configured. Cognito supports both `client_secret_post` and `client_secret_basic`. +$$ -## OIDC Token Validity +$$section +## OIDC Token Validity $(id="tokenValidity") - **Definition:** How long (in seconds) the issued tokens remain valid. - **Default:** 0 (use provider default) - **Example:** 3600 (1 hour) - **Why it matters:** Controls token lifetime and security vs usability balance. - **Note:** Use 0 to inherit Cognito's default token lifetime settings +$$ -## OIDC Custom Parameters +$$section +## OIDC Custom Parameters $(id="customParams") - **Definition:** Additional parameters to send in OIDC requests. - **Example:** {"prompt": "login", "response_type": "code"} - **Why it matters:** Allows customization of AWS Cognito authentication behavior. - **Note:** Common parameters include `prompt`, `response_type`, `scope` +$$ -## OIDC Tenant +$$section +## OIDC Tenant $(id="tenant") - **Definition:** AWS Cognito User Pool identifier. - **Example:** us-east-1_ABC123DEF - **Why it matters:** Identifies your specific Cognito User Pool. - **Note:** Your User Pool ID from AWS Console +$$ -## OIDC Max Age +$$section +## OIDC Max Age $(id="maxAge") - **Definition:** Maximum authentication age (in seconds) before re-authentication is required. - **Example:** 3600 - **Why it matters:** Controls how often users must re-authenticate. - **Note:** Leave empty for no specific max age requirement +$$ -## OIDC Prompt +$$section +## OIDC Prompt $(id="prompt") - **Definition:** Controls AWS Cognito's authentication prompts. - **Options:** none | login | consent | select_account @@ -160,23 +192,29 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - `login`: Always prompt for credentials - `consent`: Prompt for permissions (not commonly used with Cognito) - `none`: Don't show prompts (SSO only) +$$ -## OIDC Session Expiry +$$section +## OIDC Session Expiry $(id="sessionExpiry") - **Definition:** How long (in seconds) user sessions remain valid. - **Default:** 604800 (7 days) - **Example:** 604800 - **Why it matters:** Controls how often users need to re-authenticate. - **Note:** Only applies to confidential clients +$$ -## Public Key URLs +$$section +## Public Key URLs $(id="publicKeyUrls") - **Definition:** List of URLs where AWS Cognito publishes its public keys for token verification. - **Example:** ["https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123DEF/.well-known/jwks.json"] - **Why it matters:** Used to verify JWT token signatures from AWS Cognito. - **Note:** Usually auto-discovered from the discovery URI, rarely needs manual configuration +$$ -## JWT Principal Claims +$$section +## JWT Principal Claims $(id="jwtPrincipalClaims") > ⚠️ **CRITICAL WARNING**: Incorrect claims will **lock out ALL users including admins**! > - These claims MUST exist in JWT tokens from AWS Cognito @@ -189,8 +227,10 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - **Example:** ["cognito:username", "email", "sub"] - **Why it matters:** Determines which claim from the JWT token identifies the user. - **Note:** Common Cognito claims: cognito:username, email, sub, preferred_username +$$ -## JWT Principal Claims Mapping +$$section +## JWT Principal Claims Mapping $(id="jwtPrincipalClaimsMapping") - **Definition:** Maps JWT claims to OpenMetadata user attributes. - **Example:** ["email:email", "username:cognito:username"] @@ -200,8 +240,10 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - Both `username` and `email` mappings must be present when this field is used - Only `username` and `email` keys are allowed; no other keys are permitted - If validation fails, errors will be displayed on this specific field +$$ -## Token Validation Algorithm +$$section +## Token Validation Algorithm $(id="tokenValidationAlgorithm") - **Definition:** Algorithm used to validate JWT token signatures. - **Options:** RS256 | RS384 | RS512 @@ -209,3 +251,4 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - **Example:** RS256 - **Why it matters:** Must match the algorithm used by AWS Cognito to sign tokens. - **Note:** AWS Cognito uses RS256 +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/awsCognitoSSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/awsCognitoSSOClientConfig.md index e4ce4583b9b4..e3f8939ee05d 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/awsCognitoSSOClientConfig.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/awsCognitoSSOClientConfig.md @@ -1,21 +1,19 @@ ---- -title: AWS Cognito SSO Configuration | OpenMetadata -description: Configure AWS Cognito Single Sign-On for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/aws-cognito-sso ---- # AWS Cognito SSO Configuration AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credentials using OAuth 2.0 and OpenID Connect (OIDC). -## Provider Name +$$section +## Provider Name $(id="providerName") - **Definition:** A human-readable name for this AWS Cognito SSO configuration instance. - **Example:** AWS Cognito SSO, Company Cognito, User Pool Authentication - **Why it matters:** Helps identify this specific SSO configuration in logs and user interfaces. - **Note:** This is a display name and doesn't affect authentication functionality. +$$ -## Client Type +$$section +## Client Type $(id="clientType") - **Definition:** Defines whether the application is public (no client secret) or confidential (requires client secret). - **Options:** Public | Confidential @@ -24,23 +22,29 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - **Note:** - Choose **Public** for SPAs and mobile apps - Choose **Confidential** for backend services and web applications +$$ -## Enable Self Signup +$$section +## Enable Self Signup $(id="selfSignup") - **Definition:** Allows users to automatically create accounts on first login. - **Options:** Enabled | Disabled - **Example:** Enabled - **Why it matters:** Controls whether new users can join automatically or need manual approval. - **Note:** Must also be enabled in your Cognito User Pool settings. +$$ -## OIDC Client ID +$$section +## OIDC Client ID $(id="clientId") - **Definition:** App client ID from your AWS Cognito User Pool. - **Example:** 1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p - **Why it matters:** AWS Cognito uses this to identify your application during authentication. - **Note:** Found in AWS Console → Cognito → User Pools → Your pool → App integration → App clients +$$ -## OIDC Callback URL / Redirect URI +$$section +## OIDC Callback URL / Redirect URI $(id="callbackUrl") - **Definition:** URL where AWS Cognito redirects after authentication. - **Auto-Generated:** This field is automatically populated as `{your-domain}/callback`. @@ -50,15 +54,19 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - **This field is read-only** - it cannot be edited - **Copy this exact URL** and add it to AWS Cognito's allowed redirect URIs list - Format is always: `{your-domain}/callback` +$$ -## Authority +$$section +## Authority $(id="authority") - **Definition:** AWS Cognito User Pool domain that issues tokens. - **Example:** https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123DEF - **Why it matters:** Tells OpenMetadata which Cognito User Pool to authenticate against. - **Note:** Format: https://cognito-idp.{region}.amazonaws.com/{user-pool-id} +$$ -## OIDC Client Secret +$$section +## OIDC Client Secret $(id="clientSecret") - **Definition:** App client secret for confidential client authentication with AWS Cognito. - **Example:** 1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7a8b9c0d1e2f3g4h @@ -67,84 +75,104 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - Only shown for Confidential client type - Generate in Cognito → User Pool → App client → Generate client secret - Store securely and rotate regularly +$$ -## OIDC Request Scopes +$$section +## OIDC Request Scopes $(id="scopes") - **Definition:** Permissions requested from AWS Cognito during authentication. - **Default:** openid email profile - **Example:** openid email profile aws.cognito.signin.user.admin - **Why it matters:** Determines what user information OpenMetadata can access. - **Note:** Must be configured in your Cognito User Pool app client settings +$$ -## OIDC Discovery URI +$$section +## OIDC Discovery URI $(id="discoveryUri") - **Definition:** AWS Cognito's OpenID Connect metadata endpoint. - **Example:** https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123DEF/.well-known/openid_configuration - **Why it matters:** Allows OpenMetadata to automatically discover Cognito's OIDC endpoints. - **Note:** Replace {region} and {user-pool-id} with your actual values +$$ -## OIDC Use Nonce +$$section +## OIDC Use Nonce $(id="useNonce") - **Definition:** Security feature to prevent replay attacks in OIDC flows. - **Default:** false - **Example:** false - **Why it matters:** Enhances security by ensuring each authentication request is unique. - **Note:** Can be enabled for additional security if your provider supports it +$$ - - -## OIDC Disable PKCE +$$section +## OIDC Disable PKCE $(id="disablePkce") - **Definition:** Whether to disable Proof Key for Code Exchange (security extension). - **Default:** false - **Example:** false - **Why it matters:** PKCE adds security to the authorization code flow. - **Note:** Should typically be left enabled (false) for security +$$ -## OIDC Max Clock Skew +$$section +## OIDC Max Clock Skew $(id="maxClockSkew") - **Definition:** Maximum allowed time difference between systems when validating tokens. - **Example:** 0 (seconds) - **Why it matters:** Prevents token validation failures due to minor time differences. - **Note:** Usually 0 is fine unless you have significant clock skew issues +$$ -## OIDC Client Authentication Method +$$section +## OIDC Client Authentication Method $(id="clientAuthenticationMethod") - **Definition:** Method used to authenticate the client with AWS Cognito. - **Default:** client_secret_post (automatically configured) - **Why it matters:** OpenMetadata uses `client_secret_post` which is supported by AWS Cognito. - **Note:** This field is hidden and automatically configured. Cognito supports both `client_secret_post` and `client_secret_basic`. +$$ -## OIDC Token Validity +$$section +## OIDC Token Validity $(id="tokenValidity") - **Definition:** How long (in seconds) the issued tokens remain valid. - **Default:** 0 (use provider default) - **Example:** 3600 (1 hour) - **Why it matters:** Controls token lifetime and security vs usability balance. - **Note:** Use 0 to inherit Cognito's default token lifetime settings +$$ -## OIDC Custom Parameters +$$section +## OIDC Custom Parameters $(id="customParams") - **Definition:** Additional parameters to send in OIDC requests. - **Example:** {"prompt": "login", "response_type": "code"} - **Why it matters:** Allows customization of AWS Cognito authentication behavior. - **Note:** Common parameters include `prompt`, `response_type`, `scope` +$$ -## OIDC Tenant +$$section +## OIDC Tenant $(id="tenant") - **Definition:** AWS Cognito User Pool identifier. - **Example:** us-east-1_ABC123DEF - **Why it matters:** Identifies your specific Cognito User Pool. - **Note:** Your User Pool ID from AWS Console +$$ -## OIDC Max Age +$$section +## OIDC Max Age $(id="maxAge") - **Definition:** Maximum authentication age (in seconds) before re-authentication is required. - **Example:** 3600 - **Why it matters:** Controls how often users must re-authenticate. - **Note:** Leave empty for no specific max age requirement +$$ -## OIDC Prompt +$$section +## OIDC Prompt $(id="prompt") - **Definition:** Controls AWS Cognito's authentication prompts. - **Options:** none | login | consent | select_account @@ -154,23 +182,29 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - `login`: Always prompt for credentials - `consent`: Prompt for permissions (not commonly used with Cognito) - `none`: Don't show prompts (SSO only) +$$ -## OIDC Session Expiry +$$section +## OIDC Session Expiry $(id="sessionExpiry") - **Definition:** How long (in seconds) user sessions remain valid. - **Default:** 604800 (7 days) - **Example:** 604800 - **Why it matters:** Controls how often users need to re-authenticate. - **Note:** Only applies to confidential clients +$$ -## Public Key URLs +$$section +## Public Key URLs $(id="publicKey") - **Definition:** List of URLs where AWS Cognito publishes its public keys for token verification. - **Example:** ["https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123DEF/.well-known/jwks.json"] - **Why it matters:** Used to verify JWT token signatures from AWS Cognito. - **Note:** Usually auto-discovered from the discovery URI, rarely needs manual configuration +$$ -## JWT Principal Claims +$$section +## JWT Principal Claims $(id="principals") > ⚠️ **CRITICAL WARNING**: Incorrect claims will **lock out ALL users including admins**! > - These claims MUST exist in JWT tokens from AWS Cognito @@ -184,8 +218,10 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - **Why it matters:** Determines which claim from the JWT token identifies the user. - **Note:** Common Cognito claims: cognito:username, email, sub, preferred_username - Order matters - first matching claim is used +$$ -## JWT Principal Claims Mapping +$$section +## JWT Principal Claims Mapping $(id="jwtPrincipalClaimsMapping") - **Definition:** Maps JWT claims to OpenMetadata user attributes. (Overrides JWT Principal Claims if set) - **Example:** ["email:email", "username:preferred_username"] @@ -196,8 +232,10 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - Only `username` and `email` keys are allowed; no other keys are permitted - If validation fails, errors will be displayed on this specific field - **Important:** JWT Principal Claims Mapping is **rarely needed** for most AWS Cognito configurations. The default JWT Principal Claims (`email`, `cognito:username`, `sub`) handle user identification correctly. Only configure this if you have specific custom claim requirements. +$$ -## JWT Team Claim Mapping +$$section +## JWT Team Claim Mapping $(id="jwtTeamClaimMapping") - **Definition:** AWS Cognito claim or attribute containing team/department information for automatic team assignment. - **Example:** "custom:department", "custom:organization", "cognito:groups" @@ -220,29 +258,37 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - Multiple team assignments are supported for array claims (e.g., "cognito:groups") ## Authorizer Configuration +$$ -### Admin Principals +$$section +### Admin Principals $(id="adminPrincipals") - **Definition:** List of user principals who will have admin access. - **Example:** ["admin", "superuser", "john.doe"] - **Why it matters:** These users will have full administrative privileges in OpenMetadata. - **Note:** Use usernames (NOT email addresses) - these are derived from the email prefix (part before @) +$$ -### Principal Domain +$$section +### Principal Domain $(id="principalDomain") - **Definition:** Default domain for user principals. - **Example:** company.com - **Why it matters:** Used to construct full user principals when only username is provided. - **Note:** Typically your organization's primary domain +$$ -### Enforce Principal Domain +$$section +### Enforce Principal Domain $(id="enforcePrincipalDomain") - **Definition:** Whether to enforce that all users belong to the principal domain. - **Default:** false - **Example:** true - **Why it matters:** Adds an extra layer of security by restricting access to users from specific domains. +$$ -### Allowed Domains +$$section +### Allowed Domains $(id="allowedDomains") - **Definition:** List of email domains that are permitted to access OpenMetadata. - **Example:** ["company.com", "partner.com"] @@ -252,12 +298,14 @@ AWS Cognito SSO enables users to log in with their AWS Cognito User Pool credent - When `enforcePrincipalDomain` is enabled, only users with email addresses from these domains can access OpenMetadata - Leave empty or use single `principalDomain` if you only have one Cognito User Pool - Useful when your User Pool contains users from multiple domains +$$ -### Enable Secure Socket Connection +$$section +### Enable Secure Socket Connection $(id="enableSecureSocketConnection") - **Definition:** Whether to use SSL/TLS for secure connections. - **Default:** false - **Example:** true - **Why it matters:** Ensures encrypted communication for security. - **Note:** Should be enabled in production environments - +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/azureSSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/azureSSOClientConfig.md index ca30e5312486..7471720983ac 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/azureSSOClientConfig.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/azureSSOClientConfig.md @@ -1,21 +1,19 @@ ---- -title: Azure AD SSO Configuration | OpenMetadata -description: Configure Azure Active Directory Single Sign-On for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/azure-ad-sso ---- Azure Active Directory (Azure AD) SSO enables users to log in with their Microsoft 365 / Entra ID accounts using OAuth 2.0 and OpenID Connect (OIDC). ## Authentication Configuration -### Provider Name +$$section +### Provider Name $(id="providerName") - **Definition:** A human-readable name for this Azure AD SSO configuration instance. - **Example:** Azure AD SSO, Company Azure AD, Microsoft Entra ID - **Why it matters:** Helps identify this specific SSO configuration in logs and user interfaces. - **Note:** This is a display name and doesn't affect authentication functionality. +$$ -### Client Type +$$section +### Client Type $(id="clientType") - **Definition:** Defines whether the application is public (no client secret) or confidential (requires client secret). - **Options:** Public | Confidential @@ -25,23 +23,29 @@ Azure Active Directory (Azure AD) SSO enables users to log in with their Microso - Choose **Public** for SPAs and mobile apps - Choose **Confidential** for backend services and web applications - Azure typically uses **Confidential** client type +$$ -### Enable Self Signup +$$section +### Enable Self Signup $(id="selfSignup") - **Definition:** Allows users to automatically create accounts on first login. - **Options:** Enabled | Disabled - **Example:** Enabled - **Why it matters:** Controls whether new users can join automatically or need manual approval. - **Note:** Disable for stricter control over user access. +$$ -### Client ID +$$section +### Client ID $(id="clientId") - **Definition:** Application (client) ID assigned to your app in Azure AD. - **Example:** 12345678-1234-1234-1234-123456789012 - **Why it matters:** Azure AD uses this to identify your application during authentication. - **Note:** Found in Azure AD → App registrations → Your app → Overview → Application (client) ID +$$ -### Callback URL +$$section +### Callback URL $(id="callbackUrl") - **Definition:** Redirect URI where Azure AD sends authentication responses. - **Example:** https://yourapp.company.com/callback @@ -49,8 +53,10 @@ Azure Active Directory (Azure AD) SSO enables users to log in with their Microso - **Note:** - Must be registered in Azure AD → App registrations → Authentication → Redirect URIs - Always use HTTPS in production +$$ -### Authority +$$section +### Authority $(id="authority") - **Definition:** Azure AD endpoint that issues tokens for your tenant. - **Example:** https://login.microsoftonline.com/your-tenant-id @@ -58,15 +64,19 @@ Azure Active Directory (Azure AD) SSO enables users to log in with their Microso - **Note:** - Replace `your-tenant-id` with your actual Azure AD tenant ID - For multi-tenant apps, you can use `common` instead of tenant ID +$$ -### Public Key URLs +$$section +### Public Key URLs $(id="publicKey") - **Definition:** List of URLs where Azure AD publishes its public keys for token verification. - **Example:** ["https://login.microsoftonline.com/YOUR-TENANT-ID/discovery/v2.0/keys"] - **Why it matters:** Used to verify JWT token signatures from Azure AD. - **Note:** Usually auto-discovered from the discovery URI, rarely needs manual configuration +$$ -### JWT Principal Claims +$$section +### JWT Principal Claims $(id="principals") > ⚠️ **CRITICAL WARNING**: Incorrect claims will **lock out ALL users including admins**! > - These claims MUST exist in JWT tokens from Azure AD @@ -80,8 +90,10 @@ Azure Active Directory (Azure AD) SSO enables users to log in with their Microso - **Why it matters:** Determines which claim from the JWT token identifies the user. - **Note:** Common Azure AD claims: email, preferred_username, upn, sub - Order matters; first matching claim is used +$$ -### JWT Principal Claims Mapping +$$section +### JWT Principal Claims Mapping $(id="jwtPrincipalClaimsMapping") - **Definition:** Maps JWT claims to OpenMetadata user attributes. (Overrides JWT Principal Claims if set) - **Example:** ["email:email", "username:preferred_username"] @@ -92,8 +104,10 @@ Azure Active Directory (Azure AD) SSO enables users to log in with their Microso - Only `username` and `email` keys are allowed; no other keys are permitted - If validation fails, errors will be displayed on this specific field - **Important:** JWT Principal Claims Mapping is **rarely needed** for most Azure AD configurations. The default JWT Principal Claims (`preferred_username`, `email`, `upn`, `sub`) handle user identification correctly. Only configure this if you have specific custom claim requirements. +$$ -### JWT Team Claim Mapping +$$section +### JWT Team Claim Mapping $(id="jwtTeamClaimMapping") - **Definition:** Azure AD claim or attribute containing team/department information for automatic team assignment. - **Example:** "department" or "jobTitle" or "companyName" or "groups" @@ -121,15 +135,19 @@ Azure Active Directory (Azure AD) SSO enables users to log in with their Microso ## OIDC Configuration (Confidential Client Only) These fields are only shown when Client Type is set to **Confidential**. +$$ -### OIDC Client ID +$$section +### OIDC Client ID $(id="id") - **Definition:** Application (client) ID for OIDC authentication with Azure AD. - **Example:** 12345678-1234-1234-1234-123456789012 - **Why it matters:** Identifies your application to Azure AD in OIDC flows. - **Note:** Same as the Client ID in Azure AD app registration +$$ -### OIDC Client Secret +$$section +### OIDC Client Secret $(id="clientSecret") - **Definition:** Secret key for confidential client authentication with Azure AD. - **Example:** abc123def456ghi789jkl012mno345pqr678st @@ -138,74 +156,94 @@ These fields are only shown when Client Type is set to **Confidential**. - Generate in Azure AD → App registrations → Certificates & secrets - Store securely and rotate regularly - Only shown for Confidential client type +$$ -### OIDC Request Scopes +$$section +### OIDC Request Scopes $(id="scopes") - **Definition:** Permissions requested from Azure AD during authentication. - **Default:** openid email profile - **Example:** openid email profile User.Read - **Why it matters:** Determines what user information OpenMetadata can access. - **Note:** `openid email profile` are typically sufficient for most use cases +$$ -### OIDC Discovery URI +$$section +### OIDC Discovery URI $(id="discoveryUri") - **Definition:** Azure AD's OpenID Connect metadata endpoint. - **Example:** https://login.microsoftonline.com/your-tenant-id/v2.0/.well-known/openid-configuration - **Why it matters:** Allows OpenMetadata to automatically discover Azure AD's OIDC endpoints. - **Note:** Replace `your-tenant-id` with your actual tenant ID +$$ -### OIDC Use Nonce +$$section +### OIDC Use Nonce $(id="useNonce") - **Definition:** Security feature to prevent replay attacks in OIDC flows. - **Default:** false - **Example:** false - **Why it matters:** Enhances security by ensuring each authentication request is unique. - **Note:** Can be enabled for additional security if your provider supports it +$$ -### OIDC Disable PKCE +$$section +### OIDC Disable PKCE $(id="disablePkce") - **Definition:** Whether to disable Proof Key for Code Exchange (security extension). - **Default:** false - **Example:** false - **Why it matters:** PKCE adds security to the authorization code flow. - **Note:** Should typically be left enabled (false) for security +$$ -### OIDC Max Clock Skew +$$section +### OIDC Max Clock Skew $(id="maxClockSkew") - **Definition:** Maximum allowed time difference between systems when validating tokens. - **Example:** 0 (seconds) - **Why it matters:** Prevents token validation failures due to minor time differences. - **Note:** Usually 0 is fine unless you have significant clock skew issues +$$ -### OIDC Client Authentication Method +$$section +### OIDC Client Authentication Method $(id="clientAuthenticationMethod") - **Definition:** Method used to authenticate the client with Azure AD. - **Default:** client_secret_post (automatically configured) - **Why it matters:** OpenMetadata uses `client_secret_post` which is supported by Azure AD. - **Note:** This field is hidden and automatically configured. Azure AD supports both `client_secret_post` and `client_secret_basic`. +$$ -### OIDC Token Validity +$$section +### OIDC Token Validity $(id="tokenValidity") - **Definition:** How long (in seconds) the issued tokens remain valid. - **Default:** 0 (use provider default) - **Example:** 3600 (1 hour) - **Why it matters:** Controls token lifetime and security vs usability balance. +$$ -### OIDC Custom Parameters +$$section +### OIDC Custom Parameters $(id="customParams") - **Definition:** Additional parameters to send in OIDC requests. - **Example:** {"prompt": "select_account", "domain_hint": "company.com"} - **Why it matters:** Allows customization of Azure AD authentication behavior. - **Note:** Common parameters include `prompt`, `domain_hint`, `login_hint` +$$ -### OIDC Tenant +$$section +### OIDC Tenant $(id="tenant") - **Definition:** Azure AD tenant identifier for multi-tenant applications. - **Example:** your-tenant-id or company.onmicrosoft.com - **Why it matters:** Specifies which Azure AD tenant to authenticate against. - **Note:** Can be tenant ID, domain name, or "common" for multi-tenant +$$ -### OIDC Callback URL / Redirect URI +$$section +### OIDC Callback URL / Redirect URI $(id="callbackUrl") - **Definition:** URL where Azure AD redirects after authentication. - **Auto-Generated:** This field is automatically populated as `{your-domain}/callback`. @@ -215,15 +253,19 @@ These fields are only shown when Client Type is set to **Confidential**. - **This field is read-only** - it cannot be edited - **Copy this exact URL** and add it to Azure AD → App registrations → Authentication → Redirect URIs - Format is always: `{your-domain}/callback` +$$ -### OIDC Max Age +$$section +### OIDC Max Age $(id="maxAge") - **Definition:** Maximum authentication age (in seconds) before re-authentication is required. - **Example:** 3600 - **Why it matters:** Controls how often users must re-authenticate. - **Note:** Leave empty for no specific max age requirement +$$ -### OIDC Prompt +$$section +### OIDC Prompt $(id="prompt") - **Definition:** Controls Azure AD's authentication prompts. - **Options:** none | login | consent | select_account @@ -233,8 +275,10 @@ These fields are only shown when Client Type is set to **Confidential**. - `login`: Always prompt for credentials - `consent`: Prompt for permissions - `select_account`: Show account picker +$$ -### OIDC Session Expiry +$$section +### OIDC Session Expiry $(id="sessionExpiry") - **Definition:** How long (in seconds) user sessions remain valid. - **Default:** 604800 (7 days) @@ -243,29 +287,37 @@ These fields are only shown when Client Type is set to **Confidential**. - **Note:** Only applies to confidential clients ## Authorizer Configuration +$$ -### Admin Principals +$$section +### Admin Principals $(id="adminPrincipals") - **Definition:** List of user principals who will have admin access. - **Example:** ["admin", "superuser"] - **Why it matters:** These users will have full administrative privileges in OpenMetadata. - **Note:** Use usernames (NOT email addresses) - these are derived from the email prefix (part before @) +$$ -### Principal Domain +$$section +### Principal Domain $(id="principalDomain") - **Definition:** Default domain for user principals. - **Example:** company.com - **Why it matters:** Used to construct full user principals when only username is provided. - **Note:** Typically your organization's primary domain +$$ -### Enforce Principal Domain +$$section +### Enforce Principal Domain $(id="enforcePrincipalDomain") - **Definition:** Whether to enforce that all users belong to the principal domain. - **Default:** false - **Example:** true - **Why it matters:** Adds an extra layer of security by restricting access to users from specific domains. +$$ -### Allowed Domains +$$section +### Allowed Domains $(id="allowedDomains") - **Definition:** List of email domains that are permitted to access OpenMetadata. - **Example:** ["company.com", "subsidiary.com"] @@ -275,11 +327,14 @@ These fields are only shown when Client Type is set to **Confidential**. - When `enforcePrincipalDomain` is enabled, only users with email addresses from these domains can access OpenMetadata - Leave empty or use single `principalDomain` if you only have one Azure AD tenant - Useful for multi-tenant scenarios or when allowing specific external domains +$$ -### Enable Secure Socket Connection +$$section +### Enable Secure Socket Connection $(id="enableSecureSocketConnection") - **Definition:** Whether to use SSL/TLS for secure connections. - **Default:** false - **Example:** true - **Why it matters:** Ensures encrypted communication for security. - **Note:** Should be enabled in production environments +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/customOidcSSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/customOidcSSOClientConfig.md index 6821d2b21134..95a7efbacbae 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/customOidcSSOClientConfig.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/customOidcSSOClientConfig.md @@ -1,35 +1,37 @@ ---- -title: Custom OIDC Authentication Configuration | OpenMetadata -description: Configure Custom OIDC Authentication for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/custom-oidc-auth ---- Custom OIDC authentication enables integration with any OpenID Connect compliant identity provider. -## Client ID +$$section +## Client ID $(id="clientId") - **Definition:** OAuth2 client identifier issued by your OIDC provider. - **Example:** my-custom-oidc-client-12345 - **Why it matters:** Identifies your application to the OIDC provider. - **Required:** Yes +$$ -## Client Secret +$$section +## Client Secret $(id="clientSecret") - **Definition:** OAuth2 client secret issued by your OIDC provider. - **Example:** abc123-secret-xyz789 - **Why it matters:** Authenticates your application with the OIDC provider. - **Required:** Yes - **Security:** Keep this secret secure and never expose it in client-side code. +$$ -## Authority / Issuer URL +$$section +## Authority / Issuer URL $(id="authority") - **Definition:** Base URL of your OIDC provider's authentication server. - **Example:** https://auth.yourcompany.com or https://login.microsoftonline.com/tenant-id - **Why it matters:** OpenMetadata uses this to discover OIDC endpoints and validate tokens. - **Required:** Yes - **Note:** Must be publicly accessible and return valid OIDC discovery document at `/.well-known/openid_configuration` +$$ -## Callback URL / Redirect URI +$$section +## Callback URL / Redirect URI $(id="callbackUrl") - **Definition:** URL where the OIDC provider redirects after authentication. - **Auto-Generated:** This field is automatically populated as `{your-domain}/callback`. @@ -39,8 +41,10 @@ Custom OIDC authentication enables integration with any OpenID Connect compliant - **This field is read-only** - it cannot be edited - **Copy this exact URL** and add it to your OIDC provider's allowed redirect URIs list - Format is always: `{your-domain}/callback` +$$ -## Scopes +$$section +## Scopes $(id="scopes") - **Definition:** OAuth2 scopes to request from the OIDC provider. - **Default:** openid profile email @@ -51,15 +55,19 @@ Custom OIDC authentication enables integration with any OpenID Connect compliant - `profile`: Access to user profile information - `email`: Access to user email address - `groups`: Access to user group memberships (if supported) +$$ -## Public Key / JWK URI +$$section +## Public Key / JWK URI $(id="publicKey") - **Definition:** Public key or JSON Web Key Set URI for token validation. - **Example:** https://auth.yourcompany.com/.well-known/jwks.json - **Why it matters:** Used to verify the signature of JWT tokens. - **Note:** Usually auto-discovered from the OIDC provider's discovery document. +$$ -## JWT Principal Claims +$$section +## JWT Principal Claims $(id="principals") > ⚠️ **CRITICAL WARNING**: Incorrect claims will **lock out ALL users including admins**! > - These claims MUST exist in JWT tokens from your OIDC provider @@ -71,8 +79,10 @@ Custom OIDC authentication enables integration with any OpenID Connect compliant - **Default:** ["email", "preferred_username", "sub"] (recommended) - **Example:** ["email", "username", "sub"] - **Why it matters:** Maps JWT claims to OpenMetadata user identities. +$$ -### JWT Principal Claims Mapping +$$section +### JWT Principal Claims Mapping $(id="jwtPrincipalClaimsMapping") - **Definition:** Maps JWT claims to OpenMetadata user attributes. (Overrides JWT Principal Claims if set) - **Example:** ["email:email", "username:preferred_username"] @@ -83,8 +93,10 @@ Custom OIDC authentication enables integration with any OpenID Connect compliant - Only `username` and `email` keys are allowed; no other keys are permitted - If validation fails, errors will be displayed on this specific field - **Important:** JWT Principal Claims Mapping is **rarely needed** for most OIDC configurations. The default JWT Principal Claims handle user identification correctly. Only configure this if you have specific custom claim requirements. +$$ -## JWT Team Claim Mapping +$$section +## JWT Team Claim Mapping $(id="jwtTeamClaimMapping") - **Definition:** JWT claim or attribute containing team/department information for automatic team assignment. - **Example:** "department", "groups", "organization", "team" @@ -105,15 +117,19 @@ Custom OIDC authentication enables integration with any OpenID Connect compliant - Only teams of type "Group" can be auto-assigned (not "Organization" or "BusinessUnit" teams) - Team names are case-sensitive and must match exactly - Multiple team assignments are supported for array claims (e.g., "groups") +$$ -## Principal Domain +$$section +## Principal Domain $(id="principalDomain") - **Definition:** Domain to append to usernames if not present in claims. - **Example:** company.com - **Why it matters:** Ensures consistent user identification across systems. - **Optional:** Only needed if usernames don't include domain information. +$$ -### Allowed Domains +$$section +### Allowed Domains $(id="allowedDomains") - **Definition:** List of email domains that are permitted to access OpenMetadata. - **Example:** ["company.com", "external-partner.com"] @@ -123,23 +139,29 @@ Custom OIDC authentication enables integration with any OpenID Connect compliant - When `enforcePrincipalDomain` is enabled, only users with email addresses from these domains can access OpenMetadata - Leave empty or use single `principalDomain` if you only have one domain - Useful for multi-domain organizations or when integrating with providers that support multiple domains +$$ -## Admin Principals +$$section +## Admin Principals $(id="adminPrincipals") - **Definition:** List of user principals who should have admin access. - **Example:** ["admin", "sysadmin", "john.doe"] - **Why it matters:** Grants administrative privileges to specific users. - **Note:** Use usernames (NOT email addresses) - these are derived from the email prefix (part before @) - **Security:** Ensure these users are trusted administrators. +$$ -## Enable Self Signup +$$section +## Enable Self Signup $(id="selfSignup") - **Definition:** Allow new users to create accounts through OIDC authentication. - **Default:** false - **Why it matters:** Controls whether unknown users can automatically create accounts. - **Security consideration:** Enable only if you trust all users from your OIDC provider. +$$ -## Custom Provider Name +$$section +## Custom Provider Name $(id="providerName") - **Definition:** Display name for your custom OIDC provider. - **Example:** "Company SSO" or "Internal Auth" @@ -175,4 +197,5 @@ Scopes: openid profile email **Token validation errors:** Check that the token validation algorithm matches your provider and the public key/JWK URI is accessible. -**User mapping issues:** Review your JWT principal claims configuration to ensure proper user identification. \ No newline at end of file +**User mapping issues:** Review your JWT principal claims configuration to ensure proper user identification. +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/googleSSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/googleSSOClientConfig.md index fac69e28f417..95b17d0793d7 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/googleSSOClientConfig.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/googleSSOClientConfig.md @@ -1,19 +1,17 @@ ---- -title: Google SSO Configuration | OpenMetadata -description: Configure Google Single Sign-On for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/google-sso ---- Google Single Sign-On (SSO) enables users to log in with their Google Workspace accounts using OAuth 2.0 and OpenID Connect (OIDC). -### Provider Name +$$section +### Provider Name $(id="providerName") - **Definition:** A human-readable name for this Google SSO configuration instance. - **Example:** Google SSO, Company Google SSO, Google Workspace - **Why it matters:** Helps identify this specific SSO configuration in logs and user interfaces. - **Note:** This is a display name and doesn't affect authentication functionality. +$$ -### Client Type +$$section +### Client Type $(id="clientType") - **Definition:** Defines whether the application is public (no client secret) or confidential (requires client secret). - **Options:** Public | Confidential @@ -23,23 +21,29 @@ Google Single Sign-On (SSO) enables users to log in with their Google Workspace - Choose **Public** for SPAs and mobile apps - Choose **Confidential** for backend services and web applications - Google typically uses **Confidential** client type +$$ -### Enable Self Signup +$$section +### Enable Self Signup $(id="selfSignup") - **Definition:** Allows users to automatically create accounts on first login. - **Options:** Enabled | Disabled - **Example:** Enabled - **Why it matters:** Controls whether new users can join automatically or need manual approval. - **Note:** Disable for stricter control over user access. +$$ -### Client ID +$$section +### Client ID $(id="clientId") - **Definition:** OAuth 2.0 client ID assigned to your application in Google Cloud Console. - **Example:** 123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com - **Why it matters:** Google uses this to identify your application during authentication. - **Note:** Found in Google Cloud Console → APIs & Services → Credentials → OAuth 2.0 Client IDs +$$ -### Client Secret +$$section +### Client Secret $(id="secretKey") - **Definition:** Secret key for confidential client authentication with Google. - **Example:** GOCSPX-abcdefghijklmnopqrstuvwxyz123456 @@ -48,8 +52,10 @@ Google Single Sign-On (SSO) enables users to log in with their Google Workspace - Generate in Google Cloud Console → APIs & Services → Credentials - Store securely and rotate regularly - Only shown for Confidential client type +$$ -### Callback URL +$$section +### Callback URL $(id="callbackUrl") - **Definition:** Redirect URI where Google sends authentication responses. - **Example:** https://yourapp.company.com/callback @@ -57,31 +63,39 @@ Google Single Sign-On (SSO) enables users to log in with their Google Workspace - **Note:** - Must be registered in Google Cloud Console → Credentials → OAuth 2.0 Client → Authorized redirect URIs - Always use HTTPS in production +$$ -### Audience +$$section +### Audience $(id="audience") - **Definition:** Google OAuth 2.0 token endpoint URL for token validation. - **Default:** https://www.googleapis.com/oauth2/v4/token - **Example:** https://www.googleapis.com/oauth2/v4/token - **Why it matters:** Used to verify that tokens are intended for your application. - **Note:** Usually the default value is correct and doesn't need to be changed +$$ -### Authority +$$section +### Authority $(id="authority") - **Definition:** Google's authorization server endpoint for OAuth 2.0 authentication. - **Default:** https://accounts.google.com - **Example:** https://accounts.google.com - **Why it matters:** Specifies the Google authorization server that will handle authentication requests. - **Note:** This is Google's standard OAuth 2.0 authorization endpoint and typically doesn't need to be changed +$$ -### Public Key URLs +$$section +### Public Key URLs $(id="publicKey") - **Definition:** List of URLs where Google publishes its public keys for token verification. - **Example:** ["https://www.googleapis.com/oauth2/v3/certs"] - **Why it matters:** Used to verify JWT token signatures from Google. - **Note:** Usually auto-discovered from the discovery URI, rarely needs manual configuration +$$ -### JWT Principal Claims +$$section +### JWT Principal Claims $(id="principals") > ⚠️ **CRITICAL WARNING**: Incorrect claims will **lock out ALL users including admins**! > - These claims MUST exist in JWT tokens from Google @@ -94,8 +108,10 @@ Google Single Sign-On (SSO) enables users to log in with their Google Workspace - **Example:** ["email", "sub", "preferred_username"] - **Why it matters:** Determines which claim from the JWT token identifies the user. - **Note:** Common claims: email (recommended), sub, preferred_username +$$ -### JWT Principal Claims Mapping +$$section +### JWT Principal Claims Mapping $(id="jwtPrincipalClaimsMapping") - **Definition:** Maps JWT claims to OpenMetadata user attributes. - **Example:** ["email:email", "username:name"] @@ -106,8 +122,10 @@ Google Single Sign-On (SSO) enables users to log in with their Google Workspace - Only `username` and `email` keys are allowed; no other keys are permitted - If validation fails, errors will be displayed on this specific field - **Important:** JWT Principal Claims Mapping is **rarely needed** for most Google SSO configurations. The default JWT Principal Claims (`email`, `preferred_username`, `sub`) handle user identification correctly. Only configure this if you have specific custom claim requirements. +$$ -### JWT Team Claim Mapping +$$section +### JWT Team Claim Mapping $(id="jwtTeamClaimMapping") - **Definition:** JWT claim or attribute containing team/department information for automatic team assignment. - **Example:** "department", "groups", or "organizationalUnit" @@ -131,15 +149,19 @@ Google Single Sign-On (SSO) enables users to log in with their Google Workspace ## OIDC Configuration (Confidential Client Only) These fields are only shown when Client Type is set to **Confidential**. +$$ -### OIDC Client ID +$$section +### OIDC Client ID $(id="id") - **Definition:** OAuth 2.0 client ID for OIDC authentication with Google. - **Example:** 123456789012-abcdefghijklmnopqrstuvwxyz123456.apps.googleusercontent.com - **Why it matters:** Identifies your application to Google in OIDC flows. - **Note:** Same as the Client ID in Google Cloud Console +$$ -### OIDC Client Secret +$$section +### OIDC Client Secret $(id="clientSecret") - **Definition:** Secret key for confidential client authentication with Google. - **Example:** GOCSPX-abcdefghijklmnopqrstuvwxyz123456 @@ -148,8 +170,10 @@ These fields are only shown when Client Type is set to **Confidential**. - Generate in Google Cloud Console → APIs & Services → Credentials - Store securely and rotate regularly - Only shown for Confidential client type +$$ -### OIDC Request Scopes +$$section +### OIDC Request Scopes $(id="scopes") - **Definition:** Permissions requested from Google during authentication. - **Default:** openid email profile @@ -159,52 +183,66 @@ These fields are only shown when Client Type is set to **Confidential**. - `openid` is required for OIDC - `email` and `profile` provide basic user information - Additional scopes can be added based on requirements +$$ -### OIDC Discovery URI +$$section +### OIDC Discovery URI $(id="discoveryUri") - **Definition:** Google's OpenID Connect metadata endpoint. - **Example:** https://accounts.google.com/.well-known/openid-configuration - **Why it matters:** Allows OpenMetadata to automatically discover Google's OIDC endpoints. - **Note:** Google's standard discovery endpoint, rarely needs to be changed +$$ -### OIDC Use Nonce +$$section +### OIDC Use Nonce $(id="useNonce") - **Definition:** Security feature to prevent replay attacks in OIDC flows. - **Default:** false - **Example:** false - **Why it matters:** Enhances security by ensuring each authentication request is unique. - **Note:** Can be enabled for additional security if your provider supports it +$$ -### OIDC Disable PKCE +$$section +### OIDC Disable PKCE $(id="disablePkce") - **Definition:** Whether to disable Proof Key for Code Exchange (security extension). - **Default:** false - **Example:** false - **Why it matters:** PKCE adds security to the authorization code flow. - **Note:** Should typically be left enabled (false) for security +$$ -### OIDC Max Clock Skew +$$section +### OIDC Max Clock Skew $(id="maxClockSkew") - **Definition:** Maximum allowed time difference between systems when validating tokens. - **Example:** 0 (seconds) - **Why it matters:** Prevents token validation failures due to minor time differences. - **Note:** Usually 0 is fine unless you have significant clock skew issues +$$ -### OIDC Client Authentication Method +$$section +### OIDC Client Authentication Method $(id="clientAuthenticationMethod") - **Definition:** Method used to authenticate the client with Google. - **Default:** client_secret_post (automatically configured) - **Why it matters:** OpenMetadata uses `client_secret_post` which is supported by Google OAuth. - **Note:** This field is hidden and automatically configured. Google supports both `client_secret_post` and `client_secret_basic`. +$$ -### OIDC Token Validity +$$section +### OIDC Token Validity $(id="tokenValidity") - **Definition:** How long (in seconds) the issued tokens remain valid. - **Default:** 0 (use provider default) - **Example:** 3600 (1 hour) - **Why it matters:** Controls token lifetime and security vs usability balance. +$$ -### OIDC Custom Parameters +$$section +### OIDC Custom Parameters $(id="customParams") - **Definition:** Additional parameters to send in OIDC requests. - **Example:** {"hd": "company.com", "prompt": "select_account"} @@ -213,8 +251,10 @@ These fields are only shown when Client Type is set to **Confidential**. - `hd`: Hosted domain (restrict to specific Google Workspace domain) - `prompt`: Controls authentication prompts - `login_hint`: Pre-fill email address +$$ -### OIDC Callback URL / Redirect URI +$$section +### OIDC Callback URL / Redirect URI $(id="callbackUrl") - **Definition:** URL where Google redirects after authentication. - **Auto-Generated:** This field is automatically populated as `{your-domain}/callback`. @@ -224,15 +264,19 @@ These fields are only shown when Client Type is set to **Confidential**. - **This field is read-only** - it cannot be edited - **Copy this exact URL** and add it to your Google Cloud Console → OAuth 2.0 Client → Authorized redirect URIs - Format is always: `{your-domain}/callback` +$$ -### OIDC Max Age +$$section +### OIDC Max Age $(id="maxAge") - **Definition:** Maximum authentication age (in seconds) before re-authentication is required. - **Example:** 3600 - **Why it matters:** Controls how often users must re-authenticate. - **Note:** Leave empty for no specific max age requirement +$$ -### OIDC Prompt +$$section +### OIDC Prompt $(id="prompt") - **Definition:** Controls Google's authentication prompts. - **Options:** none | login | consent | select_account @@ -243,8 +287,10 @@ These fields are only shown when Client Type is set to **Confidential**. - `consent`: Prompt for permissions - `select_account`: Show account picker - `none`: Silent authentication (may fail if user isn't logged in) +$$ -### OIDC Session Expiry +$$section +### OIDC Session Expiry $(id="sessionExpiry") - **Definition:** How long (in seconds) user sessions remain valid. - **Default:** 604800 (7 days) @@ -253,30 +299,38 @@ These fields are only shown when Client Type is set to **Confidential**. - **Note:** Only applies to confidential clients ## Authorizer Configuration +$$ -### Admin Principals +$$section +### Admin Principals $(id="adminPrincipals") - **Definition:** List of user principals who will have admin access. - **Example:** ["admin", "superuser"] - **Why it matters:** These users will have full administrative privileges in OpenMetadata. - **Note:** Use usernames (NOT email addresses) - these are derived from the email prefix (part before @) +$$ -### Principal Domain +$$section +### Principal Domain $(id="principalDomain") - **Definition:** Default domain for user principals. - **Example:** company.com - **Why it matters:** Used to construct full user principals when only username is provided. - **Note:** Typically your organization's Google Workspace domain +$$ -### Enforce Principal Domain +$$section +### Enforce Principal Domain $(id="enforcePrincipalDomain") - **Definition:** Whether to enforce that all users belong to the principal domain. - **Default:** false - **Example:** true - **Why it matters:** Adds an extra layer of security by restricting access to users from specific domains. - **Note:** Useful when combined with Google Workspace `hd` parameter +$$ -### Allowed Domains +$$section +### Allowed Domains $(id="allowedDomains") - **Definition:** List of email domains that are permitted to access OpenMetadata. - **Example:** ["company.com", "contractor-company.com"] @@ -286,8 +340,10 @@ These fields are only shown when Client Type is set to **Confidential**. - When `enforcePrincipalDomain` is enabled, only users with email addresses from these domains can access OpenMetadata - Leave empty or use single `principalDomain` if you only have one Google Workspace domain - Useful when you have multiple Google Workspace domains or want to allow specific external domains +$$ -### Enable Secure Socket Connection +$$section +### Enable Secure Socket Connection $(id="enableSecureSocketConnection") - **Definition:** Whether to use SSL/TLS for secure connections. - **Default:** false @@ -318,3 +374,4 @@ Ensure the following Google APIs are enabled in your Google Cloud Console: ### Service Account (Optional) For advanced integrations, you may need to create a service account in Google Cloud Console with appropriate permissions for accessing Google Workspace data. +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/ldapSSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/ldapSSOClientConfig.md index f90aa18ac0ba..7953aee9052e 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/ldapSSOClientConfig.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/ldapSSOClientConfig.md @@ -1,20 +1,18 @@ ---- -title: LDAP Authentication Configuration | OpenMetadata -description: Configure LDAP Authentication for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/ldap-auth ---- LDAP authentication enables users to log in with their LDAP directory credentials (Active Directory, OpenLDAP, etc.). -## Enable Self Signup +$$section +## Enable Self Signup $(id="enableSelfSignup") - **Definition:** Allows users to automatically create accounts on first LDAP login. - **Options:** Enabled | Disabled - **Example:** Enabled - **Why it matters:** Controls whether new LDAP users can join automatically or need manual approval. - **Note:** Disable for stricter control over user access +$$ -## LDAP Host +$$section +## LDAP Host $(id="host") - **Definition:** LDAP server address without scheme. - **Example:** ldap.company.com or 192.168.1.100 @@ -22,8 +20,10 @@ LDAP authentication enables users to log in with their LDAP directory credential - **Note:** - Don't include protocol (ldap:// or ldaps://) - Can be hostname or IP address +$$ -## LDAP Port +$$section +## LDAP Port $(id="port") - **Definition:** Port number for LDAP server connection. - **Example:** 389 (standard LDAP) or 636 (LDAPS) @@ -32,16 +32,20 @@ LDAP authentication enables users to log in with their LDAP directory credential - Standard LDAP: 389 - LDAPS (secure): 636 - Custom ports may be used +$$ -## Max Pool Size +$$section +## Max Pool Size $(id="maxPoolSize") - **Definition:** Maximum number of connections to maintain in the LDAP connection pool. - **Default:** 3 - **Example:** 5 - **Why it matters:** Affects performance and resource usage. - **Note:** Higher values support more concurrent users but use more resources +$$ -## Full DN Required +$$section +## Full DN Required $(id="isFullDn") - **Definition:** Whether users must provide their full Distinguished Name to login. - **Default:** false @@ -50,22 +54,28 @@ LDAP authentication enables users to log in with their LDAP directory credential - **Note:** - false: Users can login with username only - true: Users must provide full DN (e.g., cn=john,ou=users,dc=company,dc=com) +$$ -## Admin Principal DN +$$section +## Admin Principal DN $(id="dnAdminPrincipal") - **Definition:** Distinguished Name of admin user with search capabilities. - **Example:** cn=admin,ou=system,dc=company,dc=com - **Why it matters:** OpenMetadata uses this account to search for and authenticate users. - **Note:** This user needs read access to user and group entries +$$ -## Admin Password +$$section +## Admin Password $(id="dnAdminPassword") - **Definition:** Password for the LDAP admin user. - **Example:** adminPassword123 - **Why it matters:** Required for OpenMetadata to authenticate as the admin user. - **Note:** Store securely and use a dedicated service account +$$ -## SSL Enabled +$$section +## SSL Enabled $(id="sslEnabled") - **Definition:** Whether to use LDAPS (secure LDAP) connection. - **Default:** false @@ -74,22 +84,28 @@ LDAP authentication enables users to log in with their LDAP directory credential - **Note:** - true: Use LDAPS (typically port 636) - false: Use plain LDAP (typically port 389) +$$ -## User Base DN +$$section +## User Base DN $(id="userBaseDN") - **Definition:** Base Distinguished Name where user accounts are located. - **Example:** ou=users,dc=company,dc=com - **Why it matters:** Tells OpenMetadata where to search for user accounts. - **Note:** Should contain all users who need access to OpenMetadata +$$ -## Group Base DN +$$section +## Group Base DN $(id="groupBaseDN") - **Definition:** Base Distinguished Name where group objects are located. - **Example:** ou=groups,dc=company,dc=com - **Why it matters:** Used for group-based authorization and role mapping. - **Note:** Optional if not using LDAP groups for authorization +$$ -## Admin Role Name +$$section +## Admin Role Name $(id="roleAdminName") - **Definition:** Special marker used in role mapping to grant admin privileges instead of regular roles. - **Example:** Admin @@ -98,8 +114,10 @@ LDAP authentication enables users to log in with their LDAP directory credential - This is NOT an LDAP group name - It's a special string used in the Auth Roles Mapping to indicate admin access - Example: Map `cn=admins,ou=groups,dc=company,dc=com` → `["Admin"]` to grant admin privileges +$$ -## All Attribute Name +$$section +## All Attribute Name $(id="allAttributeName") - **Definition:** Special wildcard character to retrieve all attributes from LDAP group objects. - **Default:** \* @@ -107,8 +125,10 @@ LDAP authentication enables users to log in with their LDAP directory credential - **Note:** - Always use "\*" (asterisk) to retrieve all attributes - This is used internally when querying groups - you rarely need to change this +$$ -## Email Attribute Name +$$section +## Email Attribute Name $(id="mailAttributeName") - **Definition:** LDAP attribute that contains user email addresses. - **Example:** mail @@ -123,8 +143,10 @@ LDAP authentication enables users to log in with their LDAP directory credential - OpenLDAP: `mail`, `email` - **How to find in phpLDAPadmin:** Open a user object and look for the attribute containing their email address - **Validation:** OpenMetadata verifies this attribute exists on actual users before saving +$$ -## Group Attribute Name +$$section +## Group Attribute Name $(id="groupAttributeName") - **Definition:** Attribute name used to identify and filter group objects in LDAP. - **Example:** objectClass @@ -139,8 +161,10 @@ LDAP authentication enables users to log in with their LDAP directory credential 3. Use `objectClass` as the attribute name 4. Use one of its values (e.g., `groupOfNames`) as the attribute value - **Validation:** OpenMetadata verifies groups can be found with this filter +$$ -## Group Attribute Value +$$section +## Group Attribute Value $(id="groupAttributeValue") - **Definition:** Value for the group attribute to identify group objects. - **Example:** groupOfNames @@ -156,8 +180,10 @@ LDAP authentication enables users to log in with their LDAP directory credential 2. Find the `objectClass` attribute 3. Use one of the objectClass values here (e.g., `groupOfNames`) - **Validation:** OpenMetadata tests that groups exist with this combination +$$ -## Group Member Attribute Name +$$section +## Group Member Attribute Name $(id="groupMemberAttributeName") - **Definition:** Attribute in group objects that lists the members of that group. - **Example:** member @@ -173,8 +199,10 @@ LDAP authentication enables users to log in with their LDAP directory credential 3. The attribute name is what you need (e.g., `member`, `uniqueMember`) 4. Example: `member: cn=john,ou=users,dc=company,dc=com` → use `member` - **Validation:** OpenMetadata checks this attribute exists on actual group objects +$$ -## Auth Roles Mapping +$$section +## Auth Roles Mapping $(id="authRolesMapping") - **Definition:** Mapping between LDAP groups and OpenMetadata roles. - **Example:** Map "cn=admins,ou=groups,dc=company,dc=com" to "Admin" role @@ -184,8 +212,10 @@ LDAP authentication enables users to log in with their LDAP directory credential - Map to existing OpenMetadata role names - Users in mapped LDAP groups will automatically receive the corresponding roles - Validation ensures all mapped roles exist in OpenMetadata +$$ -## Auth Reassign Roles +$$section +## Auth Reassign Roles $(id="authReassignRoles") - **Definition:** Roles that should be reassigned every time user logs in. - **Example:** ["Admin", "DataConsumer"] @@ -197,8 +227,10 @@ LDAP authentication enables users to log in with their LDAP directory credential ## Authorizer Configuration The following settings control authorization and access control across OpenMetadata. These settings apply globally to all authentication providers. +$$ -### Admin Principals +$$section +### Admin Principals $(id="adminPrincipals") - **Definition:** List of user principals who will have admin access to OpenMetadata. - **Example:** ["john.doe", "jane.admin", "admin"] @@ -208,23 +240,29 @@ The following settings control authorization and access control across OpenMetad - At least one admin principal is required - **Critical:** If a user's email is `john.doe@company.com`, their username will be `john.doe` - The username is NOT derived from LDAP CN or UID attributes - only from the email address +$$ -### Principal Domain +$$section +### Principal Domain $(id="principalDomain") - **Definition:** Default domain for user principals. - **Example:** company.com - **Why it matters:** Used to construct full user principals when only username is provided. - **Note:** Typically your organization's domain +$$ -### Enforce Principal Domain +$$section +### Enforce Principal Domain $(id="enforcePrincipalDomain") - **Definition:** Whether to enforce that all users belong to the principal domain. - **Default:** false - **Example:** true - **Why it matters:** Adds an extra layer of security by restricting access to users from specific domains. - **Note:** When enabled, only users from the configured principal domain can access OpenMetadata +$$ -### Allowed Domains +$$section +### Allowed Domains $(id="allowedDomains") - **Definition:** List of email domains that are permitted to access OpenMetadata. - **Example:** ["company.com", "partner.com", "contractor-company.com"] @@ -238,21 +276,27 @@ The following settings control authorization and access control across OpenMetad --- ## Advanced Configuration +$$ -## Truststore Format +$$section +## Truststore Format $(id="truststoreFormat") - **Definition:** Format of truststore for SSL/TLS connections. - **Example:** PKCS12 or JKS - **Why it matters:** Required when using SSL and custom certificates. - **Note:** Only needed if using custom SSL certificates +$$ -## Trust Store Configuration +$$section +## Trust Store Configuration $(id="trustStoreConfig") - **Definition:** SSL truststore configuration for secure LDAP connections. - **Why it matters:** Required for LDAPS connections with custom certificates. - **Note:** Contains certificate validation settings and truststore details +$$ -## Trust Store Configuration Type +$$section +## Trust Store Configuration Type $(id="truststoreConfigType") - **Definition:** Type of SSL truststore configuration for secure LDAP connections. - **Options:** TrustAll | JVMDefault | HostName | CustomTrustStore @@ -265,78 +309,101 @@ The following settings control authorization and access control across OpenMetad - **CustomTrustStore:** Use custom certificate store ### Trust Store Types: +$$ -#### Custom Trust Manager +$$section +#### Custom Trust Manager $(id="customTrustManagerConfig") - **Definition:** Custom certificate validation configuration. - **Use case:** When using self-signed or internal CA certificates. +$$ -#### Hostname Verification +$$section +#### Hostname Verification $(id="hostNameConfig") - **Definition:** Hostname verification settings for SSL connections. - **Use case:** When certificate hostname doesn't match LDAP server hostname. +$$ -#### JVM Default Trust Store +$$section +#### JVM Default Trust Store $(id="jvmDefaultConfig") - **Definition:** Use Java's default certificate trust store. - **Use case:** When LDAP server uses publicly trusted certificates. +$$ -#### Trust All Certificates +$$section +#### Trust All Certificates $(id="trustAllConfig") - **Definition:** Accept all certificates without validation. - **Use case:** Development/testing only - NOT recommended for production. - **Security Warning:** This bypasses all SSL security checks. ### Additional Trust Store Configuration Fields +$$ -#### Verify Hostname +$$section +#### Verify Hostname $(id="verifyHostname") - **Definition:** Whether to verify the hostname in the certificate matches the LDAP server hostname. - **Default:** false - **Example:** true - **Why it matters:** Prevents man-in-the-middle attacks by ensuring certificate hostname matches. - **Note:** Enable for production security +$$ -#### Examine Validity Dates +$$section +#### Examine Validity Dates $(id="examineValidityDates") - **Definition:** Check if certificates are within their valid date range. - **Default:** false - **Example:** true - **Why it matters:** Prevents using expired or not-yet-valid certificates. - **Note:** Should be enabled in production +$$ -#### Trust Store File Path +$$section +#### Trust Store File Path $(id="trustStoreFilePath") - **Definition:** Path to the Java truststore file containing trusted CA certificates. - **Example:** /path/to/truststore.jks - **Why it matters:** Specifies which certificates are trusted for SSL connections. - **Note:** Required when using custom trust manager +$$ -#### Trust Store File Password +$$section +#### Trust Store File Password $(id="trustStoreFilePassword") - **Definition:** Password to access the truststore file. - **Example:** truststorePassword123 - **Why it matters:** Required to read certificates from the truststore. - **Note:** Store securely and use strong passwords +$$ -#### Trust Store File Format +$$section +#### Trust Store File Format $(id="trustStoreFileFormat") - **Definition:** Format of the truststore file. - **Example:** JKS or PKCS12 - **Why it matters:** Tells the system how to read the truststore file. - **Note:** JKS is the traditional Java format, PKCS12 is the modern standard +$$ -#### Allow Wildcards +$$section +#### Allow Wildcards $(id="allowWildCards") - **Definition:** Whether to accept wildcard certificates (\*.company.com). - **Default:** false - **Example:** true - **Why it matters:** Controls acceptance of wildcard SSL certificates. - **Note:** Enable if your LDAP server uses wildcard certificates +$$ -#### Acceptable Host Names +$$section +#### Acceptable Host Names $(id="acceptableHostNames") - **Definition:** List of hostnames that are acceptable for certificate validation. - **Example:** ["ldap.company.com", "ldap-backup.company.com"] - **Why it matters:** Defines which hostnames are trusted for connections. - **Note:** Add all valid LDAP server hostnames +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/oktaSSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/oktaSSOClientConfig.md index c684d28c9089..a89ffa4cc76a 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/oktaSSOClientConfig.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/oktaSSOClientConfig.md @@ -1,21 +1,19 @@ ---- -title: Okta SSO Configuration | OpenMetadata -description: Configure Okta Single Sign-On for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/okta-sso ---- Okta SSO enables users to log in with their Okta credentials using OAuth 2.0 and OpenID Connect (OIDC). ## Authentication Configuration -### Provider Name +$$section +### Provider Name $(id="providerName") - **Definition:** A human-readable name for this Okta SSO configuration instance. - **Example:** Okta SSO, Company Okta, Corporate Identity - **Why it matters:** Helps identify this specific SSO configuration in logs and user interfaces. - **Note:** This is a display name and doesn't affect authentication functionality. +$$ -### Client Type +$$section +### Client Type $(id="clientType") - **Definition:** Defines whether the application is public (no client secret) or confidential (requires client secret). - **Options:** Public | Confidential @@ -25,23 +23,29 @@ Okta SSO enables users to log in with their Okta credentials using OAuth 2.0 and - Choose **Public** for SPAs and mobile apps - Choose **Confidential** for backend services and web applications - Okta typically uses **Confidential** client type +$$ -### Enable Self Signup +$$section +### Enable Self Signup $(id="selfSignup") - **Definition:** Allows users to automatically create accounts on first login. - **Options:** Enabled | Disabled - **Example:** Enabled - **Why it matters:** Controls whether new users can join automatically or need manual approval. - **Note:** Disable for stricter control over user access. +$$ -### Client ID +$$section +### Client ID $(id="clientId") - **Definition:** Client ID assigned to your app in Okta. - **Example:** 0oabc123def456ghi789 - **Why it matters:** Okta uses this to identify your application during authentication. - **Note:** Found in Okta Admin Console → Applications → Your app → General → Client ID +$$ -### Callback URL +$$section +### Callback URL $(id="callbackUrl") - **Definition:** Redirect URI where Okta sends authentication responses. - **Example:** https://yourapp.company.com/callback @@ -49,22 +53,28 @@ Okta SSO enables users to log in with their Okta credentials using OAuth 2.0 and - **Note:** - Must be registered in Okta → Applications → Your app → General → Sign-in redirect URIs - Always use HTTPS in production +$$ -### Authority +$$section +### Authority $(id="authority") - **Definition:** Okta domain that issues tokens for your organization. - **Example:** https://dev-123456.okta.com or https://company.okta.com - **Why it matters:** Tells OpenMetadata which Okta org to authenticate against. - **Note:** Use your full Okta domain URL +$$ -### Public Key URLs +$$section +### Public Key URLs $(id="publicKey") - **Definition:** List of URLs where Okta publishes its public keys for token verification. - **Example:** ["https://dev-123456.okta.com/oauth2/v1/keys"] - **Why it matters:** Used to verify JWT token signatures from Okta. - **Note:** Usually auto-discovered from the discovery URI, rarely needs manual configuration +$$ -### JWT Principal Claims +$$section +### JWT Principal Claims $(id="principals") > ⚠️ **CRITICAL WARNING**: Incorrect claims will **lock out ALL users including admins**! > - These claims MUST exist in JWT tokens from Okta @@ -78,8 +88,10 @@ Okta SSO enables users to log in with their Okta credentials using OAuth 2.0 and - **Why it matters:** Determines which claim from the JWT token identifies the user. - **Note:** Common Okta claims: email, preferred_username, sub, login - Order matters; first matching claim is used +$$ -### JWT Principal Claims Mapping +$$section +### JWT Principal Claims Mapping $(id="jwtPrincipalClaimsMapping") - **Definition:** Maps JWT claims to OpenMetadata user attributes. (Overrides JWT Principal Claims if set) - **Example:** ["email:email", "username:preferred_username"] @@ -90,8 +102,10 @@ Okta SSO enables users to log in with their Okta credentials using OAuth 2.0 and - Only `username` and `email` keys are allowed; no other keys are permitted - If validation fails, errors will be displayed on this specific field - **Important:** JWT Principal Claims Mapping is **rarely needed** for most Okta configurations. The default JWT Principal Claims (`email`, `preferred_username`, `sub`) handle user identification correctly. Only configure this if you have specific custom claim requirements. +$$ -### JWT Team Claim Mapping +$$section +### JWT Team Claim Mapping $(id="jwtTeamClaimMapping") - **Definition:** Okta claim or attribute containing team/department information for automatic team assignment. - **Example:** "department", "groups", "division", or custom profile attributes @@ -117,15 +131,19 @@ Okta SSO enables users to log in with their Okta credentials using OAuth 2.0 and ## OIDC Configuration (Confidential Client Only) These fields are only shown when Client Type is set to **Confidential**. +$$ -### OIDC Client ID +$$section +### OIDC Client ID $(id="id") - **Definition:** Client ID for OIDC authentication with Okta. - **Example:** 0oabc123def456ghi789 - **Why it matters:** Identifies your application to Okta in OIDC flows. - **Note:** Same as the Client ID from your Okta app registration +$$ -### OIDC Client Secret +$$section +### OIDC Client Secret $(id="clientSecret") - **Definition:** Secret key for confidential client authentication with Okta. - **Example:** abc123def456ghi789jkl012mno345pqr678st @@ -134,72 +152,87 @@ These fields are only shown when Client Type is set to **Confidential**. - Generate in Okta → Applications → Your app → General → Client secret - Store securely and rotate regularly - Only shown for Confidential client type +$$ -### OIDC Request Scopes +$$section +### OIDC Request Scopes $(id="scopes") - **Definition:** Permissions requested from Okta during authentication. - **Default:** openid email profile - **Example:** openid email profile groups - **Why it matters:** Determines what user information OpenMetadata can access. - **Note:** Add `groups` scope if you need group information for authorization +$$ -### OIDC Discovery URI +$$section +### OIDC Discovery URI $(id="discoveryUri") - **Definition:** Okta's OpenID Connect metadata endpoint. - **Example:** https://dev-123456.okta.com/.well-known/openid-configuration - **Why it matters:** Allows OpenMetadata to automatically discover Okta's OIDC endpoints. - **Note:** Replace with your actual Okta domain +$$ -### OIDC Use Nonce +$$section +### OIDC Use Nonce $(id="useNonce") - **Definition:** Security feature to prevent replay attacks in OIDC flows. - **Default:** false - **Example:** false - **Why it matters:** Enhances security by ensuring each authentication request is unique. - **Note:** Can be enabled for additional security if your provider supports it +$$ - - -### OIDC Disable PKCE +$$section +### OIDC Disable PKCE $(id="disablePkce") - **Definition:** Whether to disable Proof Key for Code Exchange (security extension). - **Default:** false - **Example:** false - **Why it matters:** PKCE adds security to the authorization code flow. - **Note:** Should typically be left enabled (false) for security +$$ -### OIDC Max Clock Skew +$$section +### OIDC Max Clock Skew $(id="maxClockSkew") - **Definition:** Maximum allowed time difference between systems when validating tokens. - **Example:** 0 (seconds) - **Why it matters:** Prevents token validation failures due to minor time differences. - **Note:** Usually 0 is fine unless you have significant clock skew issues +$$ -### OIDC Client Authentication Method +$$section +### OIDC Client Authentication Method $(id="clientAuthenticationMethod") - **Definition:** Method used to authenticate the client with Okta. - **Default:** client_secret_post - **Options:** client_secret_basic | client_secret_post | client_secret_jwt | private_key_jwt - **Example:** client_secret_post - **Why it matters:** Must match your Okta app configuration. +$$ -### OIDC Token Validity +$$section +### OIDC Token Validity $(id="tokenValidity") - **Definition:** How long (in seconds) the issued tokens remain valid. - **Default:** 0 (use provider default) - **Example:** 3600 (1 hour) - **Why it matters:** Controls token lifetime and security vs usability balance. - **Note:** Use 0 to inherit Okta's default token lifetime +$$ -### OIDC Custom Parameters +$$section +### OIDC Custom Parameters $(id="customParams") - **Definition:** Additional parameters to send in OIDC requests. - **Example:** {"prompt": "login", "max_age": "3600"} - **Why it matters:** Allows customization of Okta authentication behavior. - **Note:** Common parameters include `prompt`, `max_age`, `login_hint` +$$ - -### OIDC Callback URL / Redirect URI +$$section +### OIDC Callback URL / Redirect URI $(id="callbackUrl") - **Definition:** URL where Okta redirects after authentication. - **Auto-Generated:** This field is automatically populated as `{your-domain}/callback`. @@ -209,15 +242,19 @@ These fields are only shown when Client Type is set to **Confidential**. - **This field is read-only** - it cannot be edited - **Copy this exact URL** and add it to Okta's allowed redirect URIs list - Format is always: `{your-domain}/callback` +$$ -### OIDC Max Age +$$section +### OIDC Max Age $(id="maxAge") - **Definition:** Maximum authentication age (in seconds) before re-authentication is required. - **Example:** 3600 - **Why it matters:** Controls how often users must re-authenticate. - **Note:** Leave empty for no specific max age requirement +$$ -### OIDC Prompt +$$section +### OIDC Prompt $(id="prompt") - **Definition:** Controls Okta's authentication prompts. - **Options:** none | login | consent | select_account @@ -227,8 +264,10 @@ These fields are only shown when Client Type is set to **Confidential**. - `login`: Always prompt for credentials - `consent`: Prompt for permissions - `none`: Don't show prompts (SSO only) +$$ -### OIDC Session Expiry +$$section +### OIDC Session Expiry $(id="sessionExpiry") - **Definition:** How long (in seconds) user sessions remain valid. - **Default:** 604800 (7 days) @@ -237,29 +276,37 @@ These fields are only shown when Client Type is set to **Confidential**. - **Note:** Only applies to confidential clients ## Authorizer Configuration +$$ -### Admin Principals +$$section +### Admin Principals $(id="adminPrincipals") - **Definition:** List of user principals who will have admin access. - **Example:** ["admin", "superuser"] - **Why it matters:** These users will have full administrative privileges in OpenMetadata. - **Note:** Use usernames (NOT email addresses) - these are derived from the email prefix (part before @) +$$ -### Principal Domain +$$section +### Principal Domain $(id="principalDomain") - **Definition:** Default domain for user principals. - **Example:** company.com - **Why it matters:** Used to construct full user principals when only username is provided. - **Note:** Typically your organization's primary domain +$$ -### Enforce Principal Domain +$$section +### Enforce Principal Domain $(id="enforcePrincipalDomain") - **Definition:** Whether to enforce that all users belong to the principal domain. - **Default:** false - **Example:** true - **Why it matters:** Adds an extra layer of security by restricting access to users from specific domains. +$$ -### Allowed Domains +$$section +### Allowed Domains $(id="allowedDomains") - **Definition:** List of email domains that are permitted to access OpenMetadata. - **Example:** ["company.com", "partner.com"] @@ -269,11 +316,14 @@ These fields are only shown when Client Type is set to **Confidential**. - When `enforcePrincipalDomain` is enabled, only users with email addresses from these domains can access OpenMetadata - Leave empty or use single `principalDomain` if you only have one Okta org - Useful when your Okta org contains users from multiple domains +$$ -### Enable Secure Socket Connection +$$section +### Enable Secure Socket Connection $(id="enableSecureSocketConnection") - **Definition:** Whether to use SSL/TLS for secure connections. - **Default:** false - **Example:** true - **Why it matters:** Ensures encrypted communication for security. - **Note:** Should be enabled in production environments +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/samlSSOClientConfig.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/samlSSOClientConfig.md index 242124cbf5a0..6528e21fee08 100644 --- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/samlSSOClientConfig.md +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/SSO/samlSSOClientConfig.md @@ -1,8 +1,3 @@ ---- -title: SAML SSO Configuration | OpenMetadata -description: Configure SAML Single Sign-On for OpenMetadata with complete field reference -slug: /main-concepts/metadata-standard/schemas/security/client/saml-sso ---- SAML (Security Assertion Markup Language) SSO enables users to log in using SAML Identity Providers like Active Directory Federation Services (ADFS), Shibboleth, or other enterprise identity providers. @@ -32,7 +27,8 @@ To configure SAML authentication, follow these steps: **Important:** The SP Entity ID and ACS URL are generated automatically based on your OpenMetadata URL and cannot be changed. You must use these exact values in your IdP configuration for SAML to work. -## Enable Self Signup +$$section +## Enable Self Signup $(id="enableSelfSignup") - **Definition:** Allows users to automatically create accounts on first SAML login. - **Options:** Enabled | Disabled @@ -41,22 +37,28 @@ To configure SAML authentication, follow these steps: - **Note:** Disable for stricter control over user access ## Identity Provider (IdP) Configuration +$$ -### IdP Entity ID +$$section +### IdP Entity ID $(id="entityId") - **Definition:** Unique identifier for the Identity Provider. - **Example:** https://adfs.company.com/adfs/services/trust - **Why it matters:** SAML messages use this to identify the IdP. - **Note:** Must match exactly what's configured in your IdP +$$ -### SSO Login URL +$$section +### SSO Login URL $(id="ssoLoginUrl") - **Definition:** URL where users are redirected to authenticate with the IdP. - **Example:** https://adfs.company.com/adfs/ls/ - **Why it matters:** This is where authentication requests are sent. - **Note:** Usually provided by your IdP administrator +$$ -### IdP X509 Certificate +$$section +### IdP X509 Certificate $(id="idpX509Certificate") - **Definition:** Public certificate used to verify SAML assertions from the IdP. - **Example:** -----BEGIN CERTIFICATE-----\nMIIC...certificate content...\n-----END CERTIFICATE----- @@ -65,8 +67,10 @@ To configure SAML authentication, follow these steps: - Must be the actual certificate, not just the fingerprint - Include the BEGIN/END lines - Can be multi-line +$$ -### Name ID Format +$$section +### Name ID Format $(id="nameId") - **Definition:** Format of the SAML NameID element that identifies users. - **Default:** urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress @@ -75,8 +79,10 @@ To configure SAML authentication, follow these steps: - **Note:** Email format is most common and recommended ## Service Provider (SP) Configuration +$$ -### SP Entity ID +$$section +### SP Entity ID $(id="entityId") - **Definition:** Unique identifier for OpenMetadata as a Service Provider. - **Example:** https://openmetadata.company.com @@ -86,8 +92,10 @@ To configure SAML authentication, follow these steps: - **This field is read-only** - it cannot be edited - **Copy this value** and paste it as the Entity ID (or Application ID) in your SAML Identity Provider configuration - Must match exactly in your IdP's trusted applications list +$$ -### Assertion Consumer Service (ACS) URL +$$section +### Assertion Consumer Service (ACS) URL $(id="acs") - **Definition:** URL where the IdP sends SAML assertions after authentication. - **Example:** https://openmetadata.company.com/callback @@ -98,15 +106,19 @@ To configure SAML authentication, follow these steps: - **Copy this value** and paste it as the ACS URL (also called Reply URL, Callback URL, or Consumer URL) in your SAML Identity Provider configuration - Format is always: `{your-domain}/callback` - Must be registered exactly in your IdP configuration +$$ -### SP X509 Certificate +$$section +### SP X509 Certificate $(id="spX509Certificate") - **Definition:** Public certificate for OpenMetadata (Service Provider). - **Example:** -----BEGIN CERTIFICATE-----\nMIIC...certificate content...\n-----END CERTIFICATE----- - **Why it matters:** Used by IdP to verify signed SAML requests from OpenMetadata. - **Note:** Required if signing SAML requests +$$ -### SP Private Key +$$section +### SP Private Key $(id="spPrivateKey") - **Definition:** Private key for signing and encryption (Service Provider only). - **Example:** -----BEGIN PRIVATE KEY-----\nMIIE...private key content...\n-----END PRIVATE KEY----- @@ -116,48 +128,60 @@ To configure SAML authentication, follow these steps: - Required if signing or encryption is enabled ## Security Configuration +$$ -### Strict Mode +$$section +### Strict Mode $(id="strictMode") - **Definition:** Only accept valid signed and encrypted assertions if relevant flags are set. - **Default:** false - **Example:** true - **Why it matters:** Enhances security by enforcing signature and encryption validation. - **Note:** Enable for production environments +$$ -### Token Validity (seconds) +$$section +### Token Validity (seconds) $(id="tokenValidity") - **Definition:** Validity period (in seconds) for JWT tokens created from SAML response. - **Default:** 3600 (1 hour) - **Example:** 7200 (2 hours) - **Why it matters:** Controls how long users stay logged in after SAML authentication. - **Note:** This controls the OpenMetadata JWT token lifetime, not the SAML assertion lifetime +$$ -### Send Signed Auth Request +$$section +### Send Signed Auth Request $(id="sendSignedAuthRequest") - **Definition:** Whether to sign authentication requests sent to IdP. - **Default:** false - **Example:** true - **Why it matters:** Ensures authenticity of requests from OpenMetadata. - **Note:** Requires SP private key configuration +$$ -### Sign SP Metadata +$$section +### Sign SP Metadata $(id="signSpMetadata") - **Definition:** Whether to sign Service Provider metadata. - **Default:** false - **Example:** true - **Why it matters:** Ensures integrity of metadata exchanged with IdP. - **Note:** Recommended for production environments +$$ -### Want Assertions Signed +$$section +### Want Assertions Signed $(id="wantAssertionsSigned") - **Definition:** Require SAML assertions to be digitally signed by IdP. - **Default:** false - **Example:** true - **Why it matters:** Ensures assertions haven't been tampered with. - **Note:** Highly recommended for security +$$ -### Want Messages Signed +$$section +### Want Messages Signed $(id="wantMessagesSigned") - **Definition:** Require SAML messages to be digitally signed by IdP. - **Default:** false @@ -166,8 +190,10 @@ To configure SAML authentication, follow these steps: - **Note:** Provides additional security beyond assertion signing ## Advanced Configuration +$$ -### Debug Mode +$$section +### Debug Mode $(id="debugMode") - **Definition:** Enable debug logging for SAML authentication process. - **Default:** false @@ -182,8 +208,10 @@ To configure SAML authentication, follow these steps: ## Authorizer Configuration The following settings control authorization and access control across OpenMetadata. These settings apply globally to all authentication providers. +$$ -### Admin Principals +$$section +### Admin Principals $(id="adminPrincipals") - **Definition:** List of user principals who will have admin access to OpenMetadata. - **Example:** ["john.doe", "jane.admin", "admin"] @@ -192,23 +220,29 @@ The following settings control authorization and access control across OpenMetad - Use usernames (NOT full email addresses) - At least one admin principal is required - For SAML, username is derived from NameID (if email format, uses part before @) +$$ -### Principal Domain +$$section +### Principal Domain $(id="principalDomain") - **Definition:** Default domain for user principals. - **Example:** company.com - **Why it matters:** Used to construct full user principals when only username is provided. - **Note:** Typically your organization's domain +$$ -### Enforce Principal Domain +$$section +### Enforce Principal Domain $(id="enforcePrincipalDomain") - **Definition:** Whether to enforce that all users belong to the principal domain. - **Default:** false - **Example:** true - **Why it matters:** Adds an extra layer of security by restricting access to users from specific domains. - **Note:** When enabled, only users from the configured principal domain can access OpenMetadata +$$ -### Allowed Domains +$$section +### Allowed Domains $(id="allowedDomains") - **Definition:** List of email domains that are permitted to access OpenMetadata. - **Example:** ["company.com", "partner.com"] @@ -218,3 +252,4 @@ The following settings control authorization and access control across OpenMetad - When `enforcePrincipalDomain` is enabled, only users with email addresses from these domains can access OpenMetadata - Leave empty or use single `principalDomain` if you only have one domain - Use this field for multi-domain organizations +$$ \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SSOConfigurationForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SSOConfigurationForm.tsx index 24ee1fb1e83f..eac1ec14a18b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SSOConfigurationForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/SSOConfigurationForm.tsx @@ -697,13 +697,13 @@ const SSOConfigurationFormRJSF = ({ // Add event listeners when form is shown if (showForm) { document.addEventListener('focusin', handleDOMFocus); - document.addEventListener('click', handleDOMClick); + document.addEventListener('click', handleDOMClick, true); document.addEventListener('keydown', handleKeyDown, true); } return () => { document.removeEventListener('focusin', handleDOMFocus); - document.removeEventListener('click', handleDOMClick); + document.removeEventListener('click', handleDOMClick, true); document.removeEventListener('keydown', handleKeyDown, true); }; }, [showForm]); @@ -1146,7 +1146,8 @@ const SSOConfigurationFormRJSF = ({ ), minWidth: 400, flex: 0.5, - className: 'm-t-xs', + className: + 'service-doc-panel content-resizable-panel-container m-t-xs', }} /> @@ -1244,6 +1245,7 @@ const SSOConfigurationFormRJSF = ({ /> ), minWidth: 400, + className: 'service-doc-panel content-resizable-panel-container', }} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/sso-configuration-form.less b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/sso-configuration-form.less index 06c8dba3f1cf..e8d7e49b7daa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/sso-configuration-form.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSOConfigurationForm/sso-configuration-form.less @@ -154,11 +154,6 @@ } .sso-settings-page .content-resizable-panel-container { - .ant-card-body { - padding-bottom: 0; - padding-top: 0; - } - .form-actions-bottom { position: sticky; bottom: 0; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.constants.ts b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.constants.ts index 8ecf659092f8..1f5fc2b3b5a3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.constants.ts @@ -24,6 +24,7 @@ export const FIELD_MAPPINGS: Record = { authority: 'authority', domain: 'authority', // Auth0 domain maps to authority jwtPrincipalClaims: 'principals', + jwtPrincipalClaimsMapping: 'jwtPrincipalClaimsMapping', principalDomain: 'principalDomain', enforcePrincipalDomain: 'enforcePrincipalDomain', adminPrincipals: 'adminPrincipals', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.tsx index b2bdf2fbb434..cf3d841dc968 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/SSODocPanel.tsx @@ -11,22 +11,14 @@ * limitations under the License. */ import { Typography } from 'antd'; -import { first, last } from 'lodash'; -import { FC, useCallback, useEffect, useState } from 'react'; +import { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { - ENDS_WITH_NUMBER_REGEX, - ONEOF_ANYOF_ALLOF_REGEX, -} from '../../../constants/regex.constants'; import { AuthProvider } from '../../../generated/settings/settings'; -import { fetchMarkdownFile } from '../../../rest/miscAPI'; -import { SupportedLocales } from '../../../utils/i18next/LocalUtil.interface'; import { getProviderDisplayName, getProviderIcon, } from '../../../utils/SSOUtils'; -import Loader from '../../common/Loader/Loader'; -import RichTextEditorPreviewerV1 from '../../common/RichTextEditor/RichTextEditorPreviewerV1'; +import ServiceDocPanel from '../../common/ServiceDocPanel/ServiceDocPanel'; import './sso-doc-panel.less'; import { FIELD_MAPPINGS, PROVIDER_FILE_MAP } from './SSODocPanel.constants'; @@ -35,227 +27,72 @@ interface SSODocPanelProp { activeField?: string; } -const SSODocPanel: FC = ({ serviceName, activeField }) => { - const { i18n, t } = useTranslation(); - - const [isLoading, setIsLoading] = useState(false); - const [markdownContent, setMarkdownContent] = useState(''); +/** + * Resolves an SSO schema field name to its documentation data-id via FIELD_MAPPINGS. + * Schema field names (e.g. "secret", "redirectUrl") often differ from the data-id + * values used in the markdown docs (e.g. "clientSecret", "callbackUrl"). + */ +const resolveActiveField = (activeField?: string): string | undefined => { + if (!activeField) { + return activeField; + } - const getFieldGroup = (fieldName: string): string => { - const lowerFieldName = fieldName.toLowerCase(); + const parts = activeField.split('/'); + const fieldName = parts[parts.length - 1] ?? ''; + const lowerFieldName = fieldName.toLowerCase(); - // Direct mapping first - if (FIELD_MAPPINGS[fieldName]) { - return FIELD_MAPPINGS[fieldName]; - } + if (FIELD_MAPPINGS[fieldName]) { + return FIELD_MAPPINGS[fieldName]; + } - // Try to find partial matches - for (const [key, value] of Object.entries(FIELD_MAPPINGS)) { - if ( - lowerFieldName.includes(key.toLowerCase()) || - key.toLowerCase().includes(lowerFieldName) - ) { - return value; - } + for (const [key, value] of Object.entries(FIELD_MAPPINGS)) { + if ( + lowerFieldName.includes(key.toLowerCase()) || + key.toLowerCase().includes(lowerFieldName) + ) { + return value; } + } - return fieldName; - }; - - const getActiveFieldName = useCallback( - (activeFieldValue?: SSODocPanelProp['activeField']) => { - if (!activeFieldValue) { - return; - } - - const fieldNameArr = activeFieldValue.split('/'); - - if (ENDS_WITH_NUMBER_REGEX.test(activeFieldValue)) { - const result = fieldNameArr[1]; - - return result; - } - - const fieldName = last(fieldNameArr) ?? ''; + return activeField; +}; - if (ONEOF_ANYOF_ALLOF_REGEX.test(fieldName)) { - const result = first(fieldName.split('_')); +const SSODocPanel: FC = ({ serviceName, activeField }) => { + const { t } = useTranslation(); - return result; - } else { - return fieldName; - } - }, - [] + const resolvedField = useMemo( + () => resolveActiveField(activeField), + [activeField] ); - const fetchRequirement = async () => { - setIsLoading(true); - try { - const fileName = PROVIDER_FILE_MAP[serviceName] || serviceName; - const isEnglishLanguage = i18n.language === SupportedLocales.English; - const filePath = `${i18n.language}/SSO/${fileName}.md`; - const fallbackFilePath = `${SupportedLocales.English}/SSO/${fileName}.md`; - - const [translation, fallbackTranslation] = await Promise.allSettled([ - fetchMarkdownFile(filePath), - isEnglishLanguage - ? Promise.reject('') - : fetchMarkdownFile(fallbackFilePath), - ]); - - let response = ''; - if (translation.status === 'fulfilled') { - response = translation.value; - } else { - if (fallbackTranslation.status === 'fulfilled') { - response = fallbackTranslation.value; - } - } - - const cleanedResponse = response.replace(/^---\n[\s\S]*?\n---\n/, ''); - setMarkdownContent(cleanedResponse); - } catch (error) { - setMarkdownContent(''); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - fetchRequirement(); - }, [serviceName]); - - useEffect(() => { - const previouslyHighlighted = document.querySelectorAll( - '[data-highlighted="true"]' - ); - previouslyHighlighted.forEach((element) => { - element.removeAttribute('data-highlighted'); - }); - - const fieldName = getActiveFieldName(activeField); - - if (fieldName && markdownContent) { - // Add delay to allow ToastUI viewer to render the DOM - setTimeout(() => { - const groupName = getFieldGroup(fieldName); - - let element = document.querySelector(`[data-id="${fieldName}"]`); - - if (!element) { - element = document.querySelector(`[data-id="${groupName}"]`); - } - - if (!element) { - const possibleMatches = document.querySelectorAll('[data-id]'); - for (const match of possibleMatches) { - const dataId = match.getAttribute('data-id'); - if ( - dataId && - (dataId === fieldName || - dataId.includes(fieldName) || - fieldName.includes(dataId) || - dataId === groupName || - dataId.includes(groupName) || - groupName.includes(dataId)) - ) { - element = match; - - break; - } - } - } - - if (element) { - let targetElement: Element | null = element; - while ( - targetElement && - !['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes( - targetElement.tagName - ) - ) { - targetElement = targetElement.parentElement; - } - - const headingElement = targetElement || element; - - headingElement.scrollIntoView({ - block: 'center', - behavior: 'smooth', - inline: 'center', - }); - - // Collect all elements in the section - const sectionElements = [headingElement]; - let nextElement = headingElement.nextElementSibling; - while ( - nextElement && - !['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(nextElement.tagName) - ) { - if ( - nextElement.tagName === 'UL' || - nextElement.tagName === 'P' || - nextElement.tagName === 'DIV' - ) { - sectionElements.push(nextElement); - } - nextElement = nextElement.nextElementSibling; - } - - // Apply highlighting class to all section elements - sectionElements.forEach((sectionElement, index) => { - sectionElement.setAttribute('data-highlighted', 'true'); - - // Add position classes for seamless styling - if (index === 0) { - sectionElement.setAttribute('data-highlight-position', 'first'); - } else if (index === sectionElements.length - 1) { - sectionElement.setAttribute('data-highlight-position', 'last'); - } else { - sectionElement.setAttribute('data-highlight-position', 'middle'); - } - }); - - if (targetElement && element !== targetElement) { - element.setAttribute('data-highlighted', 'true'); - } - } - }, 100); - } - }, [activeField, getActiveFieldName, markdownContent]); - - if (isLoading) { - return ; - } + const resolvedServiceName = PROVIDER_FILE_MAP[serviceName] ?? serviceName; return ( -
-
-
- {getProviderIcon(serviceName) && ( -
- {`${serviceName} -
- )} - - {serviceName === AuthProvider.Basic - ? t('label.basic-configuration') - : `${getProviderDisplayName(serviceName)} ${t( - 'label.sso-configuration' - )}`} - -
- +
+
+ {getProviderIcon(serviceName) && ( +
+ {`${serviceName} +
+ )} + + {serviceName === AuthProvider.Basic + ? t('label.basic-configuration') + : `${getProviderDisplayName(serviceName)} ${t( + 'label.sso-configuration' + )}`} +
+
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/sso-doc-panel.less b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/sso-doc-panel.less index b4b610a2232d..bef8c8c41bc3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/sso-doc-panel.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/SettingsSso/SSODocPanel/sso-doc-panel.less @@ -14,22 +14,17 @@ @import (reference) '../../../styles/variables.less'; .sso-doc-panel { - height: 100%; - padding-top: @size-lg; display: flex; flex-direction: column; - - .sso-doc-content-wrapper { - flex: 1; - overflow-y: auto; - overflow-x: hidden; + padding-top: 24px; + .entity-summary-in-docs { + display: none; } - .sso-doc-header { display: flex; align-items: center; gap: @padding-xs; - padding: 0 @size-lg @size-lg @size-lg; + padding: 0 24px 24px; .sso-provider-icon { display: flex; @@ -45,235 +40,4 @@ font-size: 18px; } } - - .markdown-parser { - h1, - h2, - h3, - h4, - h5, - h6 { - font-size: 18px; - font-weight: 600; - line-height: @size-mlg; - padding-bottom: @size-xs; - color: @grey-800; - margin: 0; - padding: 0; - - &[data-highlighted='true'] { - background: @primary-50; - border-left: @size-xxs solid @primary-6; - - margin: 0; - - &[data-highlight-position='first'] { - padding-top: @size-xs; - padding-bottom: @size-xs; - border-bottom: none; - h3 { - padding: 0 @size-lg; - } - } - - &[data-highlight-position='middle'] { - border-radius: 0; - border-top: none; - border-bottom: none; - } - - &[data-highlight-position='last'] { - border-radius: 0 0 @border-rad-sm @border-rad-sm; - border-top: none; - margin-bottom: @size-md; - } - } - - span[data-id] { - font-weight: 600; - color: @grey-800; - } - } - h1 { - padding: 0 @size-lg; - } - h2 { - padding: 0 @size-lg @size-md @size-lg; - } - - h3 { - color: @grey-800; - font-size: 18px; - padding: 0 @size-lg; - font-weight: 600; - border-bottom: none; - padding-bottom: @padding-xs; - margin: 24px 0px 10px 0px; - - // Add margin after the entire section (after h3 and its siblings) - ~ h3 { - margin-top: @size-lg; - } - - ~ ul { - padding: 0 @size-lg 0 40px; - } - } - - h4 { - color: @grey-800; - font-size: @font-size-base; - font-weight: 600; - } - - h6 { - font-size: 18px; - } - - ul { - padding: 0 @size-lg @size-md @size-lg + @size-mlg; - margin-bottom: @size-lg; - - ul { - margin-bottom: 0; - padding-bottom: 0; - } - - &[data-highlighted='true'] { - background: @primary-50; - border-left: @size-xxs solid @primary-6; - - margin: 0; - - &[data-highlight-position='first'] { - border-bottom: none; - padding-left: @size-lg + @size-mlg; - } - - &[data-highlight-position='middle'] { - border-radius: 0; - border-top: none; - border-bottom: none; - } - - &[data-highlight-position='last'] { - border-top: none; - padding-bottom: @size-xs; - margin-bottom: @size-md; - } - } - - li { - margin-bottom: @padding-xss; - line-height: @size-sm + 2px; - font-size: @font-size-base; - font-weight: 400; - color: @grey-800; - - p { - padding: 0; - margin-bottom: 0; - } - - strong { - color: @grey-800; - font-weight: 600; - } - } - } - - p { - padding: 0 @size-lg; - margin-bottom: @size-lg; - line-height: @size-mlg; - font-size: @font-size-base; - font-weight: 400; - color: @grey-800; - - // Remove padding for paragraphs that can be highlighted - &[data-highlighted] { - padding: 0; - margin-bottom: @size-lg; - } - - &[data-highlighted='true'] { - background: @primary-50; - border: 1px solid @primary-6; - border-left: @size-xxs solid @primary-6; - padding: @padding-mlg @padding-md; - margin: 0; - - &[data-highlight-position='first'] { - border-radius: @border-rad-sm @border-rad-sm 0 0; - border-bottom: none; - margin: 0; - padding: 0; - } - - &[data-highlight-position='middle'] { - border-radius: 0; - border-top: none; - border-bottom: none; - padding: 0; - } - - &[data-highlight-position='last'] { - border-radius: 0 0 @border-rad-sm @border-rad-sm; - border-top: none; - padding: 0; - margin: 0; - } - } - } - - code { - background: @background-primary; - padding: @size-xxs / 2 @border-rad-xs - @size-xxs; - border-radius: @border-rad-xs; - font-size: @font-size-base - 1px; - font-weight: 400; - color: @grey-800; - border: 1px solid @border-color; - } - - pre { - background: @background-primary; - border: 1px solid @border-color; - border-radius: @border-rad-sm; - padding: @padding-md; - overflow-x: auto; - margin-bottom: @padding-md; - font-size: @font-size-base; - font-weight: 400; - color: @grey-800; - - code { - background: transparent; - border: none; - padding: 0; - color: @grey-800; - } - } - - .highlight-section { - background: @primary-50; - border-left: @size-xxs solid @primary-6; - padding: @padding-md; - border-radius: 0 @border-rad-sm @border-rad-sm 0; - margin-bottom: @padding-md; - - h4 { - margin-top: 0; - font-size: @font-size-base; - font-weight: 600; - color: @grey-800; - } - - p { - font-size: @font-size-base; - font-weight: 400; - color: @grey-800; - } - } - } } 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 7e6f553cdd57..22602328c9c2 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 @@ -17,55 +17,19 @@ import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ENDS_WITH_NUMBER_REGEX, - MARKDOWN_MATCH_ID, ONEOF_ANYOF_ALLOF_REGEX, } from '../../../constants/regex.constants'; import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { fetchMarkdownFile } from '../../../rest/miscAPI'; -import { MarkdownToHTMLConverter } from '../../../utils/FeedUtils'; import { SupportedLocales } from '../../../utils/i18next/LocalUtil.interface'; -import { getActiveFieldNameForAppDocs } from '../../../utils/ServiceUtils'; +import { + getActiveFieldNameForAppDocs, + processDocMarkdown, +} from '../../../utils/ServiceUtils'; import EntitySummaryPanel from '../../Explore/EntitySummaryPanel/EntitySummaryPanel.component'; import { SearchedDataProps } from '../../SearchedData/SearchedData.interface'; import Loader from '../Loader/Loader'; import './service-doc-panel.less'; - -const SECTION_BLOCK_REGEX = /\$\$section\n([\s\S]*?)\n\$\$/g; - -const processServiceDocMarkdown = (markdown: string): string => { - const parts: string[] = []; - let lastIndex = 0; - let match: RegExpExecArray | null; - - SECTION_BLOCK_REGEX.lastIndex = 0; - - while ((match = SECTION_BLOCK_REGEX.exec(markdown)) !== null) { - if (match.index > lastIndex) { - parts.push( - MarkdownToHTMLConverter.makeHtml(markdown.slice(lastIndex, match.index)) - ); - } - - const sectionContent = match[1]; - const idMatch = sectionContent.match(MARKDOWN_MATCH_ID); - const id = idMatch ? idMatch[1] : ''; - const cleanContent = sectionContent.replace(MARKDOWN_MATCH_ID, '').trim(); - - parts.push( - `
${MarkdownToHTMLConverter.makeHtml( - cleanContent - )}
` - ); - - lastIndex = match.index + match[0].length; - } - - if (lastIndex < markdown.length) { - parts.push(MarkdownToHTMLConverter.makeHtml(markdown.slice(lastIndex))); - } - - return parts.join('\n'); -}; interface ServiceDocPanelProp { serviceName: string; serviceType: string; @@ -203,7 +167,7 @@ const ServiceDocPanel: FC = ({ const processedHtml = useMemo( () => - DOMPurify.sanitize(processServiceDocMarkdown(markdownContent), { + DOMPurify.sanitize(processDocMarkdown(markdownContent), { ADD_ATTR: ['data-id', 'data-highlighted', 'target'], }), [markdownContent] 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 cdca4e5e9ee7..250d8a646689 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 @@ -76,6 +76,7 @@ ol { margin: 4px 0; padding-inline-start: 20px; + list-style: disc; } strong { 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 8414b63cc089..35778fbbe264 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -14,6 +14,8 @@ import { AxiosError } from 'axios'; import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill'; import { startCase } from 'lodash'; +import { MARKDOWN_MATCH_ID } from '../constants/regex.constants'; +import { MarkdownToHTMLConverter } from './FeedUtils'; import { ServiceTypes } from 'Models'; import React from 'react'; import { @@ -672,3 +674,44 @@ export const validateServiceName = async ( return null; }; + +const SECTION_BLOCK_REGEX = /\$\$section\n([\s\S]*?)\n\$\$/g; + +/** + * Converts markdown that uses $$section blocks into sanitizable HTML with + *
wrappers. Used by both ServiceDocPanel and SSODocPanel. + */ +export const processDocMarkdown = (markdown: string): string => { + const parts: string[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + + SECTION_BLOCK_REGEX.lastIndex = 0; + + while ((match = SECTION_BLOCK_REGEX.exec(markdown)) !== null) { + if (match.index > lastIndex) { + parts.push( + MarkdownToHTMLConverter.makeHtml(markdown.slice(lastIndex, match.index)) + ); + } + + const sectionContent = match[1]; + const idMatch = sectionContent.match(MARKDOWN_MATCH_ID); + const id = idMatch ? idMatch[1] : ''; + const cleanContent = sectionContent.replace(MARKDOWN_MATCH_ID, '').trim(); + + parts.push( + `
${MarkdownToHTMLConverter.makeHtml( + cleanContent + )}
` + ); + + lastIndex = match.index + match[0].length; + } + + if (lastIndex < markdown.length) { + parts.push(MarkdownToHTMLConverter.makeHtml(markdown.slice(lastIndex))); + } + + return parts.join('\n'); +}; From 3d571445cfd7129d44be898683fb664edbe71a8a Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 14 Apr 2026 23:27:56 +0530 Subject: [PATCH 05/12] minor styling issue --- .../ServiceDocPanel/service-doc-panel.less | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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 250d8a646689..6cfbdaf54b1f 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 @@ -17,7 +17,8 @@ .service-doc-panel { .service-doc-content { font-size: 14px; - padding: 4px 24px; + word-break: break-word; + overflow-wrap: break-word; h1, h2, @@ -129,10 +130,24 @@ font-weight: 500; } + & > * { + padding: 4px 24px; + } + + section { + padding: 0; + + & > * { + padding: 4px 24px; + } + } + section[data-highlighted='true'] { background-color: @primary-50; - border-left: 4px solid @primary-color; - padding-left: 12px; + border-left: 3px solid @primary-6; + padding-bottom: 12px; + margin-top: 12px; + transition: ease-in-out; } } From 2b6f191074c0a4262d9c9992182ece1d4986f9cb Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Wed, 15 Apr 2026 22:53:11 +0530 Subject: [PATCH 06/12] removed toast ui dependecy from css files --- .../ActivityFeedCard/activity-feed-card.style.less | 6 ------ .../resources/ui/src/components/Domain/domain.less | 10 +++------- .../common/CustomPropertyTable/property-value.less | 6 ------ .../common/ServiceDocPanel/ServiceDocPanel.test.tsx | 1 + .../components/common/TierCard/tier-card.style.less | 2 +- .../resources/ui/src/styles/components/glossary.less | 10 +++------- .../src/main/resources/ui/src/utils/ServiceUtils.tsx | 4 ++-- 7 files changed, 10 insertions(+), 29 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/activity-feed-card.style.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/activity-feed-card.style.less index 3a3552cb947b..3a2573d5c0d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/activity-feed-card.style.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/activity-feed-card.style.less @@ -17,12 +17,6 @@ padding: 18px 12px; margin: 0 12px; position: relative; - .toastui-editor-contents code { - color: black !important; - background-color: @code-background !important; - padding: 2px 4px; - font-size: 14px; - } } .thread-users-profile-pic .profile-image-span:first-child { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/domain.less b/openmetadata-ui/src/main/resources/ui/src/components/Domain/domain.less index 4d64de12f45a..609d0170a797 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/domain.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/domain.less @@ -120,17 +120,13 @@ .domain-form-container, .add-subdomain-modal, .add-data-product-modal { - .toastui-editor { - min-height: 150px !important; // overriding inline style + .block-editor-wrapper { + min-height: 150px; - .ProseMirror { + .tiptap.ProseMirror { min-height: 150px; } } - - .toastui-editor-md-preview { - min-height: 150px; - } } .ant-tooltip.domain-type-tooltip-container { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/property-value.less b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/property-value.less index 7c177944e403..e2b7f8fd6830 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/property-value.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/property-value.less @@ -103,12 +103,6 @@ line-height: 18px; } -.property-description { - .markdown-parser .toastui-editor-contents { - font-size: 13px; - } -} - .custom-property-inline-edit-container { width: 100%; flex-wrap: wrap; 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 b2a1ece894c0..82d772514733 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 @@ -34,6 +34,7 @@ jest.mock('../../../rest/miscAPI', () => ({ jest.mock('../../../utils/ServiceUtils', () => ({ getActiveFieldNameForAppDocs: jest.fn(), + processDocMarkdown: jest.fn((content: string) => content), })); jest.mock('react-i18next', () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/TierCard/tier-card.style.less b/openmetadata-ui/src/main/resources/ui/src/components/common/TierCard/tier-card.style.less index 3999caed5811..1bfe8944d109 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/TierCard/tier-card.style.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/TierCard/tier-card.style.less @@ -42,7 +42,7 @@ } .tier-card-description { - .toastui-editor-contents ul { + .tiptap.ProseMirror ul { padding-left: 18px; font-size: 12px; font-weight: 400; diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/components/glossary.less b/openmetadata-ui/src/main/resources/ui/src/styles/components/glossary.less index 9f5ea05bd52d..b436adf650c2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/components/glossary.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/components/glossary.less @@ -106,15 +106,11 @@ .glossary-richtext-editor, .edit-glossary-modal { - .toastui-editor { - min-height: 150px !important; // overriding inline style + .block-editor-wrapper { + min-height: 150px; - .ProseMirror { + .tiptap.ProseMirror { min-height: 150px; } } - - .toastui-editor-md-preview { - min-height: 150px; - } } 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 35778fbbe264..95a7ff6a7234 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -14,14 +14,13 @@ import { AxiosError } from 'axios'; import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill'; import { startCase } from 'lodash'; -import { MARKDOWN_MATCH_ID } from '../constants/regex.constants'; -import { MarkdownToHTMLConverter } from './FeedUtils'; import { ServiceTypes } from 'Models'; import React from 'react'; import { GlobalSettingOptions, GlobalSettingsMenuCategory, } from '../constants/GlobalSettings.constants'; +import { MARKDOWN_MATCH_ID } from '../constants/regex.constants'; import { SERVICE_TYPES_ENUM, SERVICE_TYPE_MAP, @@ -61,6 +60,7 @@ import { } from './CommonUtils'; import { getDashboardURL } from './DashboardServiceUtils'; import entityUtilClassBase from './EntityUtilClassBase'; +import { MarkdownToHTMLConverter } from './FeedUtils'; import { t } from './i18next/LocalUtil'; import { getBrokers } from './MessagingServiceUtils'; import { getSettingPath } from './RouterUtils'; From ffd3215278693d00b1737a290c3ea86dc14c43ab Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sun, 19 Apr 2026 13:03:17 +0530 Subject: [PATCH 07/12] added section node extension in RichTextEditorPreviewerV1 --- .../BlockEditor/BlockEditor.interface.ts | 6 +++ .../BlockEditor/Extensions/SectionNode.ts | 52 +++++++++++++++++++ .../ServiceDocPanel/ServiceDocPanel.tsx | 28 +++++----- .../ServiceDocPanel/service-doc-panel.less | 15 +++--- .../ui/src/constants/regex.constants.ts | 2 + .../utils/BlockEditorExtensionsClassBase.ts | 3 ++ .../resources/ui/src/utils/ServiceUtils.tsx | 9 ++-- 7 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/SectionNode.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 f0adc65677da..e57e3cb56609 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 @@ -52,6 +52,12 @@ export interface ExtensionOptions { utilityExtensions?: boolean; tableExtensions?: boolean; advancedContextExtensions?: boolean; + /** + * Enable section node extension to preserve
elements. + * Required when rendering connector documentation with scroll-to-field support. + * @default false + */ + enableSectionNode?: boolean; } export interface BlockEditorProps { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/SectionNode.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/SectionNode.ts new file mode 100644 index 000000000000..779a0bbcdd5d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/SectionNode.ts @@ -0,0 +1,52 @@ +/* + * 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 { Node } from '@tiptap/core'; + +const SectionNode = Node.create({ + name: 'section', + group: 'block', + content: 'block+', + + addAttributes() { + return { + 'data-id': { + default: null, + parseHTML: (element) => element.dataset.id, + renderHTML: (attributes) => { + if (!attributes['data-id']) { + return {}; + } + + return { 'data-id': attributes['data-id'] }; + }, + }, + 'data-highlighted': { + default: 'false', + parseHTML: (element) => element.dataset.highlighted ?? 'false', + renderHTML: (attributes) => ({ + 'data-highlighted': attributes['data-highlighted'], + }), + }, + }; + }, + + parseHTML() { + return [{ tag: 'section' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['section', HTMLAttributes, 0]; + }, +}); + +export default SectionNode; 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 22602328c9c2..0a2d65a99269 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 @@ -11,7 +11,6 @@ * limitations under the License. */ import { Col, Row } from 'antd'; -import DOMPurify from 'dompurify'; import { first, last, noop } from 'lodash'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,6 +28,7 @@ import { import EntitySummaryPanel from '../../Explore/EntitySummaryPanel/EntitySummaryPanel.component'; import { SearchedDataProps } from '../../SearchedData/SearchedData.interface'; import Loader from '../Loader/Loader'; +import RichTextEditorPreviewerV1 from '../RichTextEditor/RichTextEditorPreviewerV1'; import './service-doc-panel.less'; interface ServiceDocPanelProp { serviceName: string; @@ -106,20 +106,18 @@ const ServiceDocPanel: FC = ({ const [translation, fallbackTranslation] = await Promise.allSettled([ fetchMarkdownFile(filePath), isEnglishLanguage - ? Promise.reject('') + ? Promise.reject(new Error('Fallback not needed for English locale')) : fetchMarkdownFile(fallbackFilePath), ]); if (translation.status === 'fulfilled') { response = translation.value; - } else { - if (fallbackTranslation.status === 'fulfilled') { - response = fallbackTranslation.value; - } + } else if (fallbackTranslation.status === 'fulfilled') { + response = fallbackTranslation.value; } setMarkdownContent(response); - } catch (error) { + } catch { setMarkdownContent(''); } finally { setIsLoading(false); @@ -149,7 +147,7 @@ const ServiceDocPanel: FC = ({ '[data-highlighted="true"]' ); previousHighlighted.forEach((el) => { - el.removeAttribute('data-highlighted'); + (el as HTMLElement).dataset.highlighted = 'false'; }); const element = document.querySelector(`[data-id="${fieldName}"]`); @@ -159,17 +157,14 @@ const ServiceDocPanel: FC = ({ behavior: 'smooth', inline: 'center', }); - element.setAttribute('data-highlighted', 'true'); + (element as HTMLElement).dataset.highlighted = 'true'; } }); } }, [activeField, serviceType, isMarkdownReady]); const processedHtml = useMemo( - () => - DOMPurify.sanitize(processDocMarkdown(markdownContent), { - ADD_ATTR: ['data-id', 'data-highlighted', 'target'], - }), + () => processDocMarkdown(markdownContent), [markdownContent] ); @@ -186,10 +181,11 @@ 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 6cfbdaf54b1f..8add009caae6 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 @@ -20,6 +20,11 @@ word-break: break-word; overflow-wrap: break-word; + .ProseMirror > *:not(section), + section > * { + padding: @padding-xss @padding-lg; + } + h1, h2, h3, @@ -28,7 +33,7 @@ h6 { margin: 0 !important; border: none; - padding: 16px 0 4px; + padding: @padding-md @padding-lg @padding-xss !important; color: @text-color; } @@ -130,16 +135,8 @@ font-weight: 500; } - & > * { - padding: 4px 24px; - } - section { padding: 0; - - & > * { - padding: 4px 24px; - } } section[data-highlighted='true'] { 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 68e2e6f32337..279d2792e8da 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 @@ -76,3 +76,5 @@ export const LOCALE_CODE_REGEX = /^[a-z]{2}(-[A-Z]{2})?$/; // Filenames restricted to alphanumeric, hyphens, underscores, and dots for security 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; 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 dc904bcf2a76..caf39799d8c9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts @@ -41,6 +41,7 @@ import { mentionSuggestion } from '../components/BlockEditor/Extensions/mention/ import slashCommand from '../components/BlockEditor/Extensions/slash-command'; import { getSuggestionItems } from '../components/BlockEditor/Extensions/slash-command/items'; import renderItems from '../components/BlockEditor/Extensions/slash-command/renderItems'; +import SectionNode from '../components/BlockEditor/Extensions/SectionNode'; import TextHighlightView from '../components/BlockEditor/Extensions/text-highlight-view'; import { TrailingNode } from '../components/BlockEditor/Extensions/trailing-node'; import { DROP_CURSOR_COLOR } from '../constants/BlockEditor.constants'; @@ -222,6 +223,7 @@ export class BlockEditorExtensionsClassBase { utilityExtensions = true, tableExtensions = true, advancedContextExtensions = true, + enableSectionNode = false, } = options ?? {}; return [ @@ -233,6 +235,7 @@ export class BlockEditorExtensionsClassBase { ...(utilityExtensions ? this.getUtilityExtensions() : []), ...(tableExtensions ? this.getTableExtensions() : []), ...(advancedContextExtensions ? this.getAdvancedContentExtensions() : []), + ...(enableSectionNode ? [SectionNode] : []), ]; } 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 95a7ff6a7234..c15fb55a0895 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtils.tsx @@ -20,7 +20,10 @@ import { GlobalSettingOptions, GlobalSettingsMenuCategory, } from '../constants/GlobalSettings.constants'; -import { MARKDOWN_MATCH_ID } from '../constants/regex.constants'; +import { + MARKDOWN_MATCH_ID, + SECTION_BLOCK_REGEX, +} from '../constants/regex.constants'; import { SERVICE_TYPES_ENUM, SERVICE_TYPE_MAP, @@ -675,8 +678,6 @@ export const validateServiceName = async ( return null; }; -const SECTION_BLOCK_REGEX = /\$\$section\n([\s\S]*?)\n\$\$/g; - /** * Converts markdown that uses $$section blocks into sanitizable HTML with *
wrappers. Used by both ServiceDocPanel and SSODocPanel. @@ -696,7 +697,7 @@ export const processDocMarkdown = (markdown: string): string => { } const sectionContent = match[1]; - const idMatch = sectionContent.match(MARKDOWN_MATCH_ID); + const idMatch = MARKDOWN_MATCH_ID.exec(sectionContent); const id = idMatch ? idMatch[1] : ''; const cleanContent = sectionContent.replace(MARKDOWN_MATCH_ID, '').trim(); From 46cf2172086c84e7db258d0cf6bb6d0e7077f4ba Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sun, 19 Apr 2026 13:51:01 +0530 Subject: [PATCH 08/12] fixed unit test and lint issue --- .../ServiceDocPanel/ServiceDocPanel.test.tsx | 43 ++++++++----------- .../utils/BlockEditorExtensionsClassBase.ts | 2 +- 2 files changed, 18 insertions(+), 27 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 82d772514733..0feffbb24ad8 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 @@ -37,6 +37,10 @@ jest.mock('../../../utils/ServiceUtils', () => ({ processDocMarkdown: jest.fn((content: string) => content), })); +jest.mock('../RichTextEditor/RichTextEditorPreviewerV1', () => + jest.fn().mockReturnValue(
) +); + jest.mock('react-i18next', () => ({ useTranslation: () => ({ i18n: { @@ -56,8 +60,6 @@ const mockGetActiveFieldNameForAppDocs = const mockScrollIntoView = jest.fn(); const mockQuerySelector = jest.fn(); const mockQuerySelectorAll = jest.fn(); -const mockSetAttribute = jest.fn(); -const mockRemoveAttribute = jest.fn(); Object.defineProperty(window, 'requestAnimationFrame', { writable: true, @@ -74,13 +76,9 @@ Object.defineProperty(document, 'querySelectorAll', { value: mockQuerySelectorAll, }); -const createMockElement = ( - setAttribute = mockSetAttribute, - removeAttribute = mockRemoveAttribute -) => ({ +const createMockElement = () => ({ scrollIntoView: mockScrollIntoView, - setAttribute, - removeAttribute, + dataset: {} as DOMStringMap, }); const defaultProps = { @@ -161,25 +159,25 @@ describe('ServiceDocPanel Component', () => { it('should handle fetch failures gracefully', async () => { mockFetchMarkdownFile.mockRejectedValue(new Error('Network error')); - const { container } = render(); + render(); await waitFor(() => { - const docContent = container.querySelector('.service-doc-content'); - - expect(docContent).not.toBeNull(); - expect(docContent?.innerHTML).toBe(''); + expect(screen.getByTestId('service-requirements')).toBeInTheDocument(); + expect(screen.queryByTestId('loader')).not.toBeInTheDocument(); }); }); }); describe('Field Highlighting', () => { beforeEach(() => { - const mockElement = createMockElement(); - mockQuerySelector.mockReturnValue(mockElement); + mockQuerySelector.mockReturnValue(createMockElement()); mockQuerySelectorAll.mockReturnValue([createMockElement()]); }); it('should highlight and scroll to active field', async () => { + const mockElement = createMockElement(); + mockQuerySelector.mockReturnValue(mockElement); + render( ); @@ -191,10 +189,7 @@ describe('ServiceDocPanel Component', () => { behavior: 'smooth', inline: 'center', }); - expect(mockSetAttribute).toHaveBeenCalledWith( - 'data-highlighted', - 'true' - ); + expect(mockElement.dataset.highlighted).toBe('true'); }); }); @@ -221,6 +216,7 @@ describe('ServiceDocPanel Component', () => { it('should clean up previous highlights before highlighting new element', async () => { const previousElement = createMockElement(); + previousElement.dataset.highlighted = 'true'; const currentElement = createMockElement(); mockQuerySelectorAll.mockReturnValue([previousElement]); @@ -234,13 +230,8 @@ describe('ServiceDocPanel Component', () => { expect(mockQuerySelectorAll).toHaveBeenCalledWith( '[data-highlighted="true"]' ); - expect(previousElement.removeAttribute).toHaveBeenCalledWith( - 'data-highlighted' - ); - expect(currentElement.setAttribute).toHaveBeenCalledWith( - 'data-highlighted', - 'true' - ); + expect(previousElement.dataset.highlighted).toBe('false'); + expect(currentElement.dataset.highlighted).toBe('true'); }); }); 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 caf39799d8c9..20bbd9a7794d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BlockEditorExtensionsClassBase.ts @@ -38,10 +38,10 @@ 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 SectionNode from '../components/BlockEditor/Extensions/SectionNode'; import slashCommand from '../components/BlockEditor/Extensions/slash-command'; import { getSuggestionItems } from '../components/BlockEditor/Extensions/slash-command/items'; import renderItems from '../components/BlockEditor/Extensions/slash-command/renderItems'; -import SectionNode from '../components/BlockEditor/Extensions/SectionNode'; import TextHighlightView from '../components/BlockEditor/Extensions/text-highlight-view'; import { TrailingNode } from '../components/BlockEditor/Extensions/trailing-node'; import { DROP_CURSOR_COLOR } from '../constants/BlockEditor.constants'; From 2621a51773823d1b644016061bb36fc32d5c3a0d Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sun, 19 Apr 2026 14:31:58 +0530 Subject: [PATCH 09/12] addressed gitar comment --- .../src/components/common/ServiceDocPanel/ServiceDocPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0a2d65a99269..329cc19ce149 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 @@ -150,7 +150,7 @@ const ServiceDocPanel: FC = ({ (el as HTMLElement).dataset.highlighted = 'false'; }); - const element = document.querySelector(`[data-id="${fieldName}"]`); + const element = document.querySelector(`[data-id="${CSS.escape(fieldName)}"]`); if (element) { element.scrollIntoView({ block: fieldName === 'selected-entity' ? 'start' : 'center', From 93d801019e03b2e687225b8d37717984144005fa Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Sun, 19 Apr 2026 15:33:46 +0530 Subject: [PATCH 10/12] fixed unit test and lint fix --- .../common/ServiceDocPanel/ServiceDocPanel.test.tsx | 4 ++-- .../src/components/common/ServiceDocPanel/ServiceDocPanel.tsx | 4 +++- .../components/common/ServiceDocPanel/service-doc-panel.less | 2 +- 3 files changed, 6 insertions(+), 4 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 0feffbb24ad8..457677b3aa9c 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 @@ -209,7 +209,7 @@ describe('ServiceDocPanel Component', () => { 'root/config/database' ); expect(mockQuerySelector).toHaveBeenCalledWith( - '[data-id="config.database"]' + `[data-id="${CSS.escape('config.database')}"]` ); }); }); @@ -284,7 +284,7 @@ describe('ServiceDocPanel Component', () => { 'root/application/config' ); expect(mockQuerySelector).toHaveBeenCalledWith( - '[data-id="application.config"]' + `[data-id="${CSS.escape('application.config')}"]` ); }); }); 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 329cc19ce149..d0584a21c782 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 @@ -150,7 +150,9 @@ const ServiceDocPanel: FC = ({ (el as HTMLElement).dataset.highlighted = 'false'; }); - const element = document.querySelector(`[data-id="${CSS.escape(fieldName)}"]`); + const element = document.querySelector( + `[data-id="${CSS.escape(fieldName)}"]` + ); if (element) { element.scrollIntoView({ block: fieldName === 'selected-entity' ? 'start' : 'center', 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 8add009caae6..9ee0503674c7 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 @@ -81,7 +81,7 @@ ul, ol { margin: 4px 0; - padding-inline-start: 20px; + padding-inline-start: 36px; list-style: disc; } From 9a9467a09b1bcfb116b73a1f323646b2a45e6346 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 20 Apr 2026 15:30:33 +0530 Subject: [PATCH 11/12] fixed playwright failure --- .../ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts index 1bfea48c23df..0f9a72674b62 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/SearchIndexApplication.spec.ts @@ -251,7 +251,7 @@ test.describe('Search Index Application', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { await page.click('[data-testid="configuration"]'); - await expect(page.locator('#search-indexing-application')).toContainText( + await expect(page.getByTestId('service-requirements')).toContainText( 'Search Indexing Application' ); From d320b2df5bf5bdcc43a8575232c333c54cf7e788 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 20 Apr 2026 16:33:29 +0530 Subject: [PATCH 12/12] minor ui fix --- .../components/common/ServiceDocPanel/service-doc-panel.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9ee0503674c7..42101e1bc69e 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 @@ -81,7 +81,7 @@ ul, ol { margin: 4px 0; - padding-inline-start: 36px; + padding-inline-start: 42px; list-style: disc; }