diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java index 9006e7f88aa..53f81996794 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java @@ -22,6 +22,7 @@ import javax.validation.constraints.Min; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; +import javax.ws.rs.Encoded; import javax.ws.rs.GET; import javax.ws.rs.PATCH; import javax.ws.rs.POST; @@ -451,7 +452,8 @@ public class TestCaseResource extends EntityResource = ({ table }) => { if (isColumnFqn) { const colVal = [ { - name: getPartialNameFromTableFQN(entityTypeFQN, [ + name: getPartialNameFromTableFQN(getDecodedFqn(entityTypeFQN), [ FqnPart.NestedColumn, ]), url: getTableTabPath(entityTypeFQN, 'profiler'), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddDataQualityTest/components/TestCaseForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddDataQualityTest/components/TestCaseForm.tsx index 1660e9130a7..c4506a346a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AddDataQualityTest/components/TestCaseForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AddDataQualityTest/components/TestCaseForm.tsx @@ -37,7 +37,11 @@ import { TestDefinition, TestPlatform, } from '../../../generated/tests/testDefinition'; -import { getNameFromFQN } from '../../../utils/CommonUtils'; +import { + getNameFromFQN, + replaceAllSpacialCharWith_, +} from '../../../utils/CommonUtils'; +import { getDecodedFqn, getEncodedFqn } from '../../../utils/StringsUtils'; import { generateEntityLink } from '../../../utils/TableUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import RichTextEditor from '../../common/rich-text-editor/RichTextEditor'; @@ -51,6 +55,7 @@ const TestCaseForm: React.FC = ({ table, }) => { const { entityTypeFQN, dashboardType } = useParams>(); + const decodedEntityFQN = getDecodedFqn(entityTypeFQN); const isColumnFqn = dashboardType === ProfilerDashboardType.COLUMN; const [form] = Form.useForm(); const markdownRef = useRef(); @@ -72,7 +77,7 @@ const TestCaseForm: React.FC = ({ testPlatform: TestPlatform.OpenMetadata, supportedDataType: isColumnFqn ? table.columns.find( - (column) => column.fullyQualifiedName === entityTypeFQN + (column) => column.fullyQualifiedName === decodedEntityFQN )?.dataType : undefined, }); @@ -87,7 +92,7 @@ const TestCaseForm: React.FC = ({ const { data } = await getListTestCase({ fields: 'testDefinition', limit: API_RES_MAX_SIZE, - entityLink: generateEntityLink(entityTypeFQN, isColumnFqn), + entityLink: generateEntityLink(decodedEntityFQN, isColumnFqn), }); setTestCases(data); @@ -158,7 +163,10 @@ const TestCaseForm: React.FC = ({ return { name: value.testName, - entityLink: generateEntityLink(entityTypeFQN, isColumnFqn), + entityLink: generateEntityLink( + getEncodedFqn(decodedEntityFQN, true), + isColumnFqn + ), parameterValues: parameterValues as TestCaseParameterValue[], testDefinition: { id: value.testTypeId, @@ -201,13 +209,16 @@ const TestCaseForm: React.FC = ({ ); setSelectedTestType(value.testTypeId); const testCount = testCases.filter((test) => - test.name.includes(`${getNameFromFQN(entityTypeFQN)}_${testType?.name}`) + test.name.includes( + `${getNameFromFQN(decodedEntityFQN)}_${testType?.name}` + ) ); // generating dynamic unique name based on entity_testCase_number + const name = `${getNameFromFQN(decodedEntityFQN)}_${testType?.name}${ + testCount.length ? `_${testCount.length}` : '' + }`; form.setFieldsValue({ - testName: `${getNameFromFQN(entityTypeFQN)}_${testType?.name}${ - testCount.length ? `_${testCount.length}` : '' - }`, + testName: replaceAllSpacialCharWith_(name), }); } }; @@ -220,7 +231,9 @@ const TestCaseForm: React.FC = ({ fetchAllTestCases(); } form.setFieldsValue({ - testName: initialValue?.name ?? getNameFromFQN(entityTypeFQN), + testName: replaceAllSpacialCharWith_( + initialValue?.name ?? getNameFromFQN(decodedEntityFQN) + ), testTypeId: initialValue?.testDefinition?.id, params: initialValue?.parameterValues?.length ? getParamsValue() @@ -243,6 +256,10 @@ const TestCaseForm: React.FC = ({ required: true, message: 'Name is required!', }, + { + pattern: /^[A-Za-z0-9_]*$/g, + message: 'Spacial character is not allowed!', + }, { validator: (_, value) => { if (testCases.some((test) => test.name === value)) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/ProfilerDashboard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/ProfilerDashboard.tsx index eef8b07394d..59e8e4ea9c0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/ProfilerDashboard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/ProfilerDashboard.tsx @@ -49,6 +49,7 @@ import { getProfilerDashboardWithFqnPath, } from '../../utils/RouterUtils'; import { serviceTypeLogo } from '../../utils/ServiceUtils'; +import { getDecodedFqn } from '../../utils/StringsUtils'; import { generateEntityLink, getTagsWithoutTier, @@ -88,6 +89,7 @@ const ProfilerDashboard: React.FC = ({ dashboardType: ProfilerDashboardType; tab: ProfilerDashboardTab; }>(); + const decodedEntityFQN = getDecodedFqn(entityTypeFQN); const isColumnView = dashboardType === ProfilerDashboardType.COLUMN; const [follower, setFollower] = useState([]); const [isFollowing, setIsFollowing] = useState(false); @@ -155,7 +157,7 @@ const ProfilerDashboard: React.FC = ({ const breadcrumb = useMemo(() => { const serviceName = getEntityName(table.service); const fqn = table.fullyQualifiedName || ''; - const columnName = getPartialNameFromTableFQN(entityTypeFQN, [ + const columnName = getPartialNameFromTableFQN(decodedEntityFQN, [ FqnPart.NestedColumn, ]); @@ -337,7 +339,7 @@ const ProfilerDashboard: React.FC = ({ tablePermissions.ViewBasic || tablePermissions.ViewTests) ) { - fetchTestCases(generateEntityLink(entityTypeFQN, true)); + fetchTestCases(generateEntityLink(decodedEntityFQN, true)); } else if ( ProfilerDashboardTab.PROFILER === value && (tablePermissions.ViewAll || @@ -392,7 +394,7 @@ const ProfilerDashboard: React.FC = ({ useEffect(() => { if (table) { if (isColumnView) { - const columnName = getNameFromFQN(entityTypeFQN); + const columnName = getNameFromFQN(decodedEntityFQN); const selectedColumn = table.columns.find( (col) => col.name === columnName ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/component/DataQualityTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/component/DataQualityTab.tsx index 196efbdda7c..f49b40e0e24 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/component/DataQualityTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/component/DataQualityTab.tsx @@ -26,6 +26,7 @@ import { TestCase, TestCaseResult } from '../../../generated/tests/testCase'; import { useAuth } from '../../../hooks/authHooks'; import { getEntityName, getNameFromFQN } from '../../../utils/CommonUtils'; import { getTestSuitePath } from '../../../utils/RouterUtils'; +import { getDecodedFqn } from '../../../utils/StringsUtils'; import SVGIcons, { Icons } from '../../../utils/SvgUtils'; import { getEntityFqnFromEntityLink, @@ -126,15 +127,16 @@ const DataQualityTab: React.FC = ({ if (isColumn) { const name = getNameFromFQN( - getEntityFqnFromEntityLink(entityLink, isColumn) + getDecodedFqn( + getEntityFqnFromEntityLink(entityLink, isColumn), + true + ) ); return name; } - return isColumn - ? getNameFromFQN(getEntityFqnFromEntityLink(entityLink, isColumn)) - : '--'; + return '--'; }, }, { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/component/TestSummary.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/component/TestSummary.tsx index dbae4141a1e..33d9c556f85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/component/TestSummary.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ProfilerDashboard/component/TestSummary.tsx @@ -39,6 +39,7 @@ import { TestCaseResult, TestCaseStatus, } from '../../../generated/tests/testCase'; +import { getEncodedFqn } from '../../../utils/StringsUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import ErrorPlaceHolder from '../../common/error-with-placeholder/ErrorPlaceHolder'; import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer'; @@ -130,7 +131,7 @@ const TestSummary: React.FC = ({ data }) => { .unix(); const endTs = moment().unix(); const { data: chartData } = await getListTestCaseResults( - data.fullyQualifiedName || '', + getEncodedFqn(data.fullyQualifiedName || ''), { startTs, endTs, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ColumnProfileTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ColumnProfileTable.tsx index 9d52b92134e..1afaea39835 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ColumnProfileTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ColumnProfileTable.tsx @@ -33,6 +33,7 @@ import { getAddDataQualityTableTestPath, getProfilerDashboardWithFqnPath, } from '../../../utils/RouterUtils'; +import { getEncodedFqn } from '../../../utils/StringsUtils'; import SVGIcons, { Icons } from '../../../utils/SvgUtils'; import Ellipses from '../../common/Ellipses/Ellipses'; import Searchbar from '../../common/searchbar/Searchbar'; @@ -165,7 +166,9 @@ const ColumnProfileTable: FC = ({ key: 'dataQualityTest', render: (_, record) => { const summary = - columnTestSummary?.[record.fullyQualifiedName || '']?.results; + columnTestSummary?.[ + getEncodedFqn(record.fullyQualifiedName || '', true) + ]?.results; const currentResult = summary ? Object.entries(summary).map(([key, value]) => ({ value, @@ -246,7 +249,9 @@ const ColumnProfileTable: FC = ({ setData( columns.map((col) => ({ ...col, - testCount: colResult?.[col.fullyQualifiedName || '']?.count, + testCount: + colResult?.[getEncodedFqn(col.fullyQualifiedName || '', true)] + ?.count, })) ); setColumnTestSummary(colResult); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerDashboardPage/ProfilerDashboardPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerDashboardPage/ProfilerDashboardPage.tsx index 951ba6af62b..0232a5bad33 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerDashboardPage/ProfilerDashboardPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ProfilerDashboardPage/ProfilerDashboardPage.tsx @@ -43,6 +43,7 @@ import { getTableFQNFromColumnFQN, } from '../../utils/CommonUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; +import { getDecodedFqn } from '../../utils/StringsUtils'; import { generateEntityLink } from '../../utils/TableUtils'; import { showErrorToast } from '../../utils/ToastUtils'; @@ -52,6 +53,7 @@ const ProfilerDashboardPage = () => { dashboardType: ProfilerDashboardType; tab: ProfilerDashboardTab; }>(); + const decodedEntityFQN = getDecodedFqn(entityTypeFQN); const isColumnView = dashboardType === ProfilerDashboardType.COLUMN; const [table, setTable] = useState({} as Table); const [profilerData, setProfilerData] = useState([]); @@ -117,14 +119,14 @@ const ProfilerDashboardPage = () => { }; const handleTestCaseUpdate = () => { - fetchTestCases(generateEntityLink(entityTypeFQN, isColumnView)); + fetchTestCases(generateEntityLink(decodedEntityFQN, isColumnView)); }; const fetchTableEntity = async () => { try { const fqn = isColumnView - ? getTableFQNFromColumnFQN(entityTypeFQN) - : entityTypeFQN; + ? getTableFQNFromColumnFQN(decodedEntityFQN) + : decodedEntityFQN; const field = `tags, usageSummary, owner, followers${ isColumnView ? ', profile' : '' }`; @@ -159,7 +161,7 @@ const ProfilerDashboardPage = () => { tab === ProfilerDashboardTab.DATA_QUALITY && (permission.ViewAll || permission.ViewBasic || permission.ViewTests) ) { - fetchTestCases(generateEntityLink(entityTypeFQN)); + fetchTestCases(generateEntityLink(decodedEntityFQN)); } else if ( permission.ViewAll || permission.ViewBasic || @@ -172,13 +174,13 @@ const ProfilerDashboardPage = () => { }; useEffect(() => { - if (entityTypeFQN) { + if (decodedEntityFQN) { fetchTableEntity(); } else { setIsLoading(false); setError(true); } - }, [entityTypeFQN]); + }, [decodedEntityFQN]); useEffect(() => { if (!isEmpty(table)) { @@ -201,7 +203,9 @@ const ProfilerDashboardPage = () => {

No data found{' '} - {entityTypeFQN ? `for column ${getNameFromFQN(entityTypeFQN)}` : ''} + {decodedEntityFQN + ? `for column ${getNameFromFQN(decodedEntityFQN)}` + : ''}

); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index 25e72662781..a3773d63510 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -711,6 +711,10 @@ export const replaceSpaceWith_ = (text: string) => { return text.replace(/\s/g, '_'); }; +export const replaceAllSpacialCharWith_ = (text: string) => { + return text.replaceAll(/[&/\\#, +()$~%.'":*?<>{}]/g, '_'); +}; + export const getFeedCounts = ( entityType: string, entityFQN: string, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StringsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StringsUtils.ts index 358cab5c724..cd78bff4348 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StringsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StringsUtils.ts @@ -128,8 +128,29 @@ export const getErrorText = ( * @param fqn - Value to be encoded * @returns - Encoded text string as a valid component of a Uniform Resource Identifier (URI). */ -export const getEncodedFqn = (fqn: string) => { - return encodeURIComponent(fqn); +export const getEncodedFqn = (fqn: string, spaceAsPlus = false) => { + let uri = encodeURIComponent(fqn); + + if (spaceAsPlus) { + uri = uri.replaceAll('%20', '+'); + } + + return uri; +}; + +/** + * + * @param fqn - Value to be encoded + * @returns - Decode text string as a valid component of a Uniform Resource Identifier (URI). + */ +export const getDecodedFqn = (fqn: string, plusAsSpace = false) => { + let uri = decodeURIComponent(fqn); + + if (plusAsSpace) { + uri = uri.replaceAll('+', ' '); + } + + return uri; }; /**