mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 10:26:09 +00:00
Fixed issue: Quality tests can not be created if the column names have spaces or special characters #7623 (#7698)
* Fixed issue: Quality tests can not be created if the column names have spaces or special characters #7623 * Added logic to not decode fqn param from path * Removed commented out code * Allowed only "_" in spacial character in test case name Co-authored-by: Teddy Crepineau <teddy.crepineau@gmail.com>
This commit is contained in:
parent
e49ad000ee
commit
b81686b426
@ -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<TestCase, TestCaseRepositor
|
||||
public Response addTestCaseResult(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "fqn of the testCase", schema = @Schema(type = "string")) @PathParam("fqn") String fqn,
|
||||
@Encoded @Parameter(description = "fqn of the testCase", schema = @Schema(type = "string")) @PathParam("fqn")
|
||||
String fqn,
|
||||
@Valid TestCaseResult testCaseResult)
|
||||
throws IOException {
|
||||
authorizer.authorizeAdmin(securityContext, true);
|
||||
|
@ -41,6 +41,7 @@ import {
|
||||
} from '../../utils/CommonUtils';
|
||||
import { getTestSuitePath } from '../../utils/RouterUtils';
|
||||
import { serviceTypeLogo } from '../../utils/ServiceUtils';
|
||||
import { getDecodedFqn } from '../../utils/StringsUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import SuccessScreen from '../common/success-screen/SuccessScreen';
|
||||
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
|
||||
@ -103,7 +104,7 @@ const AddDataQualityTestV1: React.FC<AddDataQualityTestProps> = ({ table }) => {
|
||||
if (isColumnFqn) {
|
||||
const colVal = [
|
||||
{
|
||||
name: getPartialNameFromTableFQN(entityTypeFQN, [
|
||||
name: getPartialNameFromTableFQN(getDecodedFqn(entityTypeFQN), [
|
||||
FqnPart.NestedColumn,
|
||||
]),
|
||||
url: getTableTabPath(entityTypeFQN, 'profiler'),
|
||||
|
@ -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<TestCaseFormProps> = ({
|
||||
table,
|
||||
}) => {
|
||||
const { entityTypeFQN, dashboardType } = useParams<Record<string, string>>();
|
||||
const decodedEntityFQN = getDecodedFqn(entityTypeFQN);
|
||||
const isColumnFqn = dashboardType === ProfilerDashboardType.COLUMN;
|
||||
const [form] = Form.useForm();
|
||||
const markdownRef = useRef<EditorContentRef>();
|
||||
@ -72,7 +77,7 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
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<TestCaseFormProps> = ({
|
||||
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<TestCaseFormProps> = ({
|
||||
|
||||
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<TestCaseFormProps> = ({
|
||||
);
|
||||
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<TestCaseFormProps> = ({
|
||||
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<TestCaseFormProps> = ({
|
||||
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)) {
|
||||
|
@ -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<ProfilerDashboardProps> = ({
|
||||
dashboardType: ProfilerDashboardType;
|
||||
tab: ProfilerDashboardTab;
|
||||
}>();
|
||||
const decodedEntityFQN = getDecodedFqn(entityTypeFQN);
|
||||
const isColumnView = dashboardType === ProfilerDashboardType.COLUMN;
|
||||
const [follower, setFollower] = useState<EntityReference[]>([]);
|
||||
const [isFollowing, setIsFollowing] = useState<boolean>(false);
|
||||
@ -155,7 +157,7 @@ const ProfilerDashboard: React.FC<ProfilerDashboardProps> = ({
|
||||
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<ProfilerDashboardProps> = ({
|
||||
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<ProfilerDashboardProps> = ({
|
||||
useEffect(() => {
|
||||
if (table) {
|
||||
if (isColumnView) {
|
||||
const columnName = getNameFromFQN(entityTypeFQN);
|
||||
const columnName = getNameFromFQN(decodedEntityFQN);
|
||||
const selectedColumn = table.columns.find(
|
||||
(col) => col.name === columnName
|
||||
);
|
||||
|
@ -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<DataQualityTabProps> = ({
|
||||
|
||||
if (isColumn) {
|
||||
const name = getNameFromFQN(
|
||||
getEntityFqnFromEntityLink(entityLink, isColumn)
|
||||
getDecodedFqn(
|
||||
getEntityFqnFromEntityLink(entityLink, isColumn),
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
return isColumn
|
||||
? getNameFromFQN(getEntityFqnFromEntityLink(entityLink, isColumn))
|
||||
: '--';
|
||||
return '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -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<TestSummaryProps> = ({ data }) => {
|
||||
.unix();
|
||||
const endTs = moment().unix();
|
||||
const { data: chartData } = await getListTestCaseResults(
|
||||
data.fullyQualifiedName || '',
|
||||
getEncodedFqn(data.fullyQualifiedName || ''),
|
||||
{
|
||||
startTs,
|
||||
endTs,
|
||||
|
@ -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<ColumnProfileTableProps> = ({
|
||||
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<ColumnProfileTableProps> = ({
|
||||
setData(
|
||||
columns.map((col) => ({
|
||||
...col,
|
||||
testCount: colResult?.[col.fullyQualifiedName || '']?.count,
|
||||
testCount:
|
||||
colResult?.[getEncodedFqn(col.fullyQualifiedName || '', true)]
|
||||
?.count,
|
||||
}))
|
||||
);
|
||||
setColumnTestSummary(colResult);
|
||||
|
@ -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<Table>({} as Table);
|
||||
const [profilerData, setProfilerData] = useState<ColumnProfile[]>([]);
|
||||
@ -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 = () => {
|
||||
<ErrorPlaceHolder>
|
||||
<p className="tw-text-center">
|
||||
No data found{' '}
|
||||
{entityTypeFQN ? `for column ${getNameFromFQN(entityTypeFQN)}` : ''}
|
||||
{decodedEntityFQN
|
||||
? `for column ${getNameFromFQN(decodedEntityFQN)}`
|
||||
: ''}
|
||||
</p>
|
||||
</ErrorPlaceHolder>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user