fix(ui): add description around filter pattern (#9133)

* fix(ui): add description around filter pattern

* update localization text

* cleanup common utils

* add docs link to description

* doc : change pattern helper text as per comments

* fix : filter pattern checkbox opacity

* Fix cy tests

* Revert : filter pattern styling

* Fix : cypress test

* text updated as per comment

* fix cypress failure

* fix cypress for checkboxes

* address comments

Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
Chirag Madlani 2022-12-13 16:14:48 +05:30 committed by GitHub
parent d1a739ec55
commit 38f9e0555d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 170 additions and 161 deletions

View File

@ -186,7 +186,7 @@ export const testServiceCreationAndIngestion = (
cy.get('[data-testid="add-ingestion-container"]').should('be.visible');
if (isDatabaseService(type)) {
cy.get('[data-testid="schema-filter-pattern-checkbox"]').should(
cy.get('[data-testid="configure-ingestion-container"]').should(
'be.visible'
);

View File

@ -71,9 +71,10 @@ describe('BigQuery Ingestion', () => {
};
const addIngestionInput = () => {
cy.get('[data-testid="schema-filter-pattern-checkbox"]')
.scrollIntoView()
.should('be.visible')
.invoke('show')
.trigger('mouseover')
.check();
cy.get('[data-testid="filter-pattern-includes-schema"]')
.scrollIntoView()

View File

@ -47,8 +47,7 @@ describe('Glue Ingestion', () => {
const addIngestionInput = () => {
cy.get('[data-testid="schema-filter-pattern-checkbox"]')
.scrollIntoView()
.should('be.visible')
.invoke('show').trigger('mouseover')
.check();
cy.get('[data-testid="filter-pattern-includes-schema"]')
.scrollIntoView()

View File

@ -40,7 +40,7 @@ describe('Kafka Ingestion', () => {
const addIngestionInput = () => {
cy.get('[data-testid="topic-filter-pattern-checkbox"]')
.should('be.visible')
.invoke('show').trigger('mouseover')
.check();
cy.get('[data-testid="filter-pattern-includes-topic"]')
.should('be.visible')

View File

@ -41,7 +41,7 @@ describe('Metabase Ingestion', () => {
const addIngestionInput = () => {
cy.get('[data-testid="dashboard-filter-pattern-checkbox"]')
.should('be.visible')
.invoke('show').trigger('mouseover')
.check();
cy.get('[data-testid="filter-pattern-includes-dashboard"]')
.should('be.visible')

View File

@ -27,7 +27,8 @@ describe('MySQL Ingestion', () => {
goToAddNewServicePage(SERVICE_TYPE.Database);
const addIngestionInput = () => {
cy.get('[data-testid="schema-filter-pattern-checkbox"]').check();
// cy.get('[data-testid="filter-pattern-container"]').first().scrollIntoView().should('be.visible');
cy.get('[data-testid="schema-filter-pattern-checkbox"]').invoke('show').trigger('mouseover').check();
cy.get('[data-testid="filter-pattern-includes-schema"]')
.should('be.visible')
.type(Cypress.env('mysqlDatabaseSchema'));

View File

@ -53,8 +53,7 @@ it('add and ingest data', () => {
const addIngestionInput = () => {
cy.get('[data-testid="schema-filter-pattern-checkbox"]')
.scrollIntoView()
.should('be.visible')
.invoke('show').trigger('mouseover')
.check();
cy.get('[data-testid="filter-pattern-includes-schema"]')
.scrollIntoView()

View File

@ -48,7 +48,7 @@ describe('RedShift Ingestion', () => {
const addIngestionInput = () => {
// no schema or database filters
cy.get('[data-testid="schema-filter-pattern-checkbox"]').check();
cy.get('[data-testid="schema-filter-pattern-checkbox"]').invoke('show').trigger('mouseover').check();
cy.get('[data-testid="filter-pattern-includes-schema"]')
.should('be.visible')
.type('dbt_jaffle');

View File

@ -35,7 +35,7 @@ describe('Snowflake Ingestion', () => {
};
const addIngestionInput = () => {
cy.get('[data-testid="schema-filter-pattern-checkbox"]').check();
cy.get('[data-testid="schema-filter-pattern-checkbox"]').invoke('show').trigger('mouseover').check();
cy.get('[data-testid="filter-pattern-includes-schema"]')
.should('be.visible')
.type(schema);

View File

@ -50,7 +50,7 @@ describe('Superset Ingestion', () => {
const addIngestionInput = () => {
cy.get('[data-testid="dashboard-filter-pattern-checkbox"]')
.should('be.visible')
.invoke('show').trigger('mouseover')
.check();
cy.get('[data-testid="filter-pattern-includes-dashboard"]')
.should('be.visible')

View File

@ -104,7 +104,11 @@ describe('Data Quality and Profiler should work properly', () => {
.scrollIntoView()
.contains('Profiler Ingestion')
.click();
cy.get('[data-testid="profileSample"]').should('be.visible').and('not.be.disabled').type(10);
cy.get('[data-testid="profileSample"]')
.scrollIntoView()
.should('be.visible')
.and('not.be.disabled')
.type(10);
cy.get('[data-testid="next-button"]')
.scrollIntoView()
.should('be.visible')

View File

@ -942,11 +942,11 @@ const Users = ({
) : (
<ErrorPlaceHolder>
{tabNumber === 3
? t('server.no-user-entities', {
type: 'owned',
? t('server.you-have-not-action-anything-yet', {
action: t('label.owned-lowercase'),
})
: t('server.no-user-entities', {
type: 'followed',
: t('server.you-have-not-action-anything-yet', {
action: t('label.followed-lowercase'),
})}
</ErrorPlaceHolder>
)}

View File

@ -55,4 +55,43 @@ describe('Test FilterPattern component', () => {
expect(includeFilterInput).toBeInTheDocument();
expect(excludeFilterInput).toBeInTheDocument();
});
it('FilterPattern component should render with filter pattern description', async () => {
const { container } = render(<FilterPattern {...mockFilterPatternProps} />);
const filterPatternContainer = await findByTestId(
container,
'filter-pattern-container'
);
const fieldContainer = await findByTestId(container, 'field-container');
const checkbox = await findByTestId(
container,
`${mockFilterPatternProps.type}-filter-pattern-checkbox`
);
const includeFilterInfo = await findByTestId(
container,
'filter-pattern-include-info'
);
const excludeFilterInfo = await findByTestId(
container,
'filter-pattern-exclude-info'
);
const includeFilterInput = await findByTestId(
container,
'filter-pattern-includes-table'
);
const excludeFilterInput = await findByTestId(
container,
'filter-pattern-excludes-table'
);
expect(includeFilterInfo).toBeInTheDocument();
expect(excludeFilterInfo).toBeInTheDocument();
expect(filterPatternContainer).toBeInTheDocument();
expect(checkbox).toBeInTheDocument();
expect(fieldContainer).toBeInTheDocument();
expect(includeFilterInput).toBeInTheDocument();
expect(excludeFilterInput).toBeInTheDocument();
});
});

View File

@ -11,9 +11,15 @@
* limitations under the License.
*/
import { capitalize } from 'lodash';
import { Checkbox, Col, Input, Row, Typography } from 'antd';
import { t } from 'i18next';
import { capitalize, toLower } from 'lodash';
import React from 'react';
import { getSeparator } from '../../../utils/CommonUtils';
import {
getFilterPatternDocsLinks,
getSeparator,
} from '../../../utils/CommonUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { Field } from '../../Field/Field';
import { FilterPatternProps } from './filterPattern.interface';
@ -42,41 +48,77 @@ const FilterPattern = ({
};
return (
<div className="tw-mt-4" data-testid="filter-pattern-container">
<div className="tw-flex tw-items-center">
<input
checked={checked}
className="tw-mr-3 custom-checkbox"
data-testid={`${type}-filter-pattern-checkbox`}
id={`${type}FilterPatternCheckbox`}
name={`${type}FilterPatternCheckbox`}
type="checkbox"
onChange={(e) => handleChecked(e.target.checked)}
/>
<label htmlFor={`${type}FilterPatternCheckbox`}>{`${capitalize(
type
)} Filter Pattern`}</label>
</div>
<div className="m-t-md" data-testid="filter-pattern-container">
<Row>
<Col>
<Checkbox
checked={checked}
className="m-r-sm filter-pattern-checkbox"
data-testid={`${type}-filter-pattern-checkbox`}
id={`${type}FilterPatternCheckbox`}
name={`${type}FilterPatternCheckbox`}
onChange={(e) => handleChecked(e.target.checked)}
/>
</Col>
<Col className="flex flex-col">
<label htmlFor={`${type}FilterPatternCheckbox`}>{`${capitalize(
type
)} Filter Pattern`}</label>
<Typography.Text
className="text-grey-muted m-t-xss"
data-testid="filter-pattern-info">
{t('message.filter-pattern-info', {
filterPattern: type,
})}{' '}
<Typography.Link
href={getFilterPatternDocsLinks(type)}
target="_blank">
{t('label.read-more')}{' '}
<SVGIcons
alt="external-link"
className="m-l-xss"
icon={Icons.EXTERNAL_LINK}
width="14px"
/>
</Typography.Link>
</Typography.Text>
</Col>
</Row>
{checked && (
<div data-testid="field-container">
<Field>
<label className="tw-block tw-form-label">Include:</label>
<input
className="tw-form-inputs tw-relative tw-form-inputs-padding tw-py-2"
<label className="flex flex-col">{t('label.include')}:</label>
<Typography.Text
className="text-grey-muted m-t-xss m-b-xss"
data-testid="filter-pattern-include-info">
{t('message.filter-pattern-include-exclude-info', {
activity: toLower(t('label.include')),
filterPattern: type,
})}
</Typography.Text>
<Input
className="m-t-xss"
data-testid={`filter-pattern-includes-${type}`}
placeholder="Enter a list of strings/regex patterns as a comma separated value"
placeholder={t('label.list-of-strings-regex-patterns-csv')}
type="text"
value={includePattern}
onChange={includeFilterChangeHandler}
/>
</Field>
<Field>
<label className="tw-block tw-form-label">Exclude:</label>
<input
className="tw-form-inputs tw-relative tw-form-inputs-padding tw-py-2"
<label className="flex flex-col">{t('label.exclude')}:</label>
<Typography.Text
className="text-grey-muted m-t-xss m-b-xss"
data-testid="filter-pattern-exclude-info">
{t('message.filter-pattern-include-exclude-info', {
activity: toLower(t('label.exclude')),
filterPattern: type,
})}
</Typography.Text>
<Input
className="m-t-xss"
data-testid={`filter-pattern-excludes-${type}`}
placeholder="Enter a list of strings/regex patterns as a comma separated value"
placeholder={t('label.list-of-strings-regex-patterns-csv')}
type="text"
value={excludePattern}
onChange={excludeFilterChangeHandler}

View File

@ -95,8 +95,6 @@
"no-rule-found": "No rule found",
"no-policy-found": "No policy found for",
"delete": "Delete",
"not-followed-yet": "You have not followed anything yet",
"not-owned-yet": "You have not owned anything yet",
"no-inherited-found": "No inherited roles found",
"inherited-roles": "Inherited Roles",
"roles": "Roles",
@ -234,6 +232,8 @@
"lineage": "Lineage",
"custom-properties": "Custom Properties",
"dag-view": "DAG view",
"read-more": "Read more",
"read-less": "Read less",
"read-more-lowercase": "read more",
"read-less-lowercase": "read less",
"no-entity": "No {{entity}}",
@ -380,6 +380,9 @@
"hide": "Hide",
"restore-team": "Restore Team",
"remove": "Remove",
"list-of-strings-regex-patterns-csv": "Enter a list of strings/regex patterns as a comma separated value",
"owned-lowercase": "owned",
"followed-lowercase": "followed",
"move-the-team": "Move the Team",
"data-insight-plural": "Data Insights",
"configure-entity": "Configure {{entity}}",
@ -541,6 +544,8 @@
"delete-message-question-mark": "Delete Message?",
"view-deleted-teams": "View all the Deleted Teams, which come under this Team.",
"restore-deleted-team": " Restoring the Team will add all the metadata back to OpenMetadata",
"filter-pattern-info": "Choose to include or exclude {{filterPattern}} as part of the metadata ingestion.",
"filter-pattern-include-exclude-info": "Explicitly {{activity}} {{filterPattern}} by adding a list of comma-separated regex.",
"team-moved-success": "Team moved successfully",
"team-transfer-message": "Click on Confirm if youd like to move {{from}} team under {{to}} team.",
"create-new-glossary-guide": "A Glossary is a controlled vocabulary used to define the concepts and terminology in an organization. Glossaries can be specific to a certain domain (for e.g., Business Glossary, Technical Glossary). In the glossary, the standard terms and concepts can be defined along with the synonyms, and related terms. Control can be established over how and who can add the terms in the glossary.",
@ -566,7 +571,7 @@
"delete-team-message": "Any teams under \"{{teamName}}\" will be {{deleteType}} deleted as well."
},
"server": {
"no-followed-entities": "You have not followed anything yet.",
"you-have-not-action-anything-yet": "You have not {{action}} anything yet.",
"no-owned-entities": "You have not owned anything yet.",
"no-task-available": "No task data is available",
"entity-fetch-error": "Error while fetching {{entity}}",

View File

@ -62,4 +62,5 @@ jest.mock('react-i18next', () => ({
useTranslation: jest.fn().mockReturnValue({
t: (key) => key,
}),
t: (key) => key,
}));

View File

@ -19,9 +19,7 @@ import classNames from 'classnames';
import i18n from 'i18next';
import {
capitalize,
differenceWith,
isEmpty,
isEqual,
isNil,
isNull,
isString,
@ -38,12 +36,11 @@ import {
RecentlyViewed,
RecentlyViewedData,
} from 'Models';
import React, { FormEvent } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import { reactLocalStorage } from 'reactjs-localstorage';
import AppState from '../AppState';
import { getFeedCount } from '../axiosAPIs/feedsAPI';
import { Button } from '../components/buttons/Button/Button';
import {
getDayCron,
getHourCron,
@ -57,7 +54,6 @@ import {
imageTypes,
LOCALSTORAGE_RECENTLY_SEARCHED,
LOCALSTORAGE_RECENTLY_VIEWED,
TITLE_FOR_NON_OWNER_ACTION,
} from '../constants/constants';
import {
UrlEntityCharRegEx,
@ -65,7 +61,7 @@ import {
} from '../constants/regex.constants';
import { SIZE } from '../enums/common.enum';
import { EntityType, FqnPart, TabSpecificField } from '../enums/entity.enum';
import { Ownership } from '../enums/mydata.enum';
import { FilterPatternEnum } from '../enums/filterPattern.enum';
import { Kpi } from '../generated/dataInsight/kpi/kpi';
import { Bot } from '../generated/entity/bot';
import { Dashboard } from '../generated/entity/data/dashboard';
@ -91,7 +87,6 @@ import Fqn from './Fqn';
import { LIST_CAP } from './PermissionsUtils';
import { getRoleWithFqnPath, getTeamsWithFqnPath } from './RouterUtils';
import { serviceTypeLogo } from './ServiceUtils';
import SVGIcons, { Icons } from './SvgUtils';
import { getTierFromSearchTableTags } from './TableUtils';
import { TASK_ENTITIES } from './TasksUtils';
import { showErrorToast } from './ToastUtils';
@ -362,41 +357,6 @@ export const addToRecentViewed = (eData: RecentlyViewedData): void => {
setRecentlyViewedData(recentlyViewed.data);
};
export const getHtmlForNonAdminAction = (isClaimOwner: boolean) => {
return (
<>
<p>{TITLE_FOR_NON_OWNER_ACTION}</p>
{!isClaimOwner ? <p>Claim ownership in Manage </p> : null}
</>
);
};
export const getOwnerIds = (
filter: Ownership,
userDetails: User,
nonSecureUserDetails: User
): Array<string> => {
if (filter === Ownership.OWNER) {
if (!isEmpty(userDetails)) {
return [
...(userDetails.teams?.map((team) => team.id) || []),
userDetails.id,
];
} else {
if (!isEmpty(nonSecureUserDetails)) {
return [
...(nonSecureUserDetails.teams?.map((team) => team.id) || []),
nonSecureUserDetails.id,
];
} else {
return [];
}
}
} else {
return [userDetails.id || nonSecureUserDetails.id];
}
};
export const getActiveCatClass = (name: string, activeName = '') => {
return activeName === name ? 'activeCategory' : '';
};
@ -413,18 +373,6 @@ export const errorMsg = (value: string) => {
);
};
export const validMsg = (value: string) => {
return (
<div className="tw-mt-1">
<strong
className="tw-text-success tw-text-xs tw-italic"
data-testid="error-message">
{value}
</strong>
</div>
);
};
export const requiredField = (label: string, excludeSpace = false) => (
<>
{label}{' '}
@ -470,14 +418,6 @@ export const getServiceLogo = (
return null;
};
export const getSvgArrow = (isActive: boolean) => {
return isActive ? (
<SVGIcons alt="arrow-down" icon={Icons.ARROW_DOWN_PRIMARY} />
) : (
<SVGIcons alt="arrow-right" icon={Icons.ARROW_RIGHT_PRIMARY} />
);
};
export const isValidUrl = (href?: string) => {
if (!href) {
return false;
@ -522,10 +462,6 @@ export const getFields = (defaultFields: string, tabSpecificField: string) => {
return `${defaultFields}, ${tabSpecificField}`;
};
export const restrictFormSubmit = (e: FormEvent) => {
e.preventDefault();
};
export const getEntityMissingError = (entityType: string, fqn: string) => {
return (
<p>
@ -534,47 +470,6 @@ export const getEntityMissingError = (entityType: string, fqn: string) => {
);
};
export const getDocButton = (label: string, url: string, dataTestId = '') => {
return (
<Button
className="tw-group tw-rounded tw-w-full tw-px-3 tw-py-1.5 tw-text-sm"
data-testid={dataTestId}
href={url}
rel="noopener noreferrer"
size="custom"
tag="a"
target="_blank"
theme="primary"
variant="outlined">
<SVGIcons
alt="Doc icon"
className="tw-align-middle tw-mr-2 group-hover:tw-hidden"
icon={Icons.DOC_PRIMARY}
width="14"
/>
<SVGIcons
alt="Doc icon"
className="tw-align-middle tw-mr-2 tw-hidden group-hover:tw-inline-block"
icon={Icons.DOC_WHITE}
width="14"
/>
<span>{label}</span>
<SVGIcons
alt="external-link"
className="tw-align-middle tw-ml-2 group-hover:tw-hidden"
icon={Icons.EXTERNAL_LINK}
width="14"
/>
<SVGIcons
alt="external-link"
className="tw-align-middle tw-ml-2 tw-hidden group-hover:tw-inline-block"
icon={Icons.EXTERNAL_LINK_WHITE}
width="14"
/>
</Button>
);
};
export const getNameFromFQN = (fqn: string): string => {
const arr = fqn.split(FQN_SEPARATOR_CHAR);
@ -824,13 +719,6 @@ export const getTeamsUser = (
return;
};
export const getDiffArray = (
compareWith: string[],
toCompare: string[]
): string[] => {
return differenceWith(compareWith, toCompare, isEqual);
};
export const getHostNameFromURL = (url: string) => {
if (isValidUrl(url)) {
const domain = new URL(url);
@ -1063,3 +951,33 @@ export const sortTagsCaseInsensitive = (tags: TagLabel[]) => {
tag1.tagFQN.toLowerCase() < tag2.tagFQN.toLowerCase() ? -1 : 1
);
};
/**
* It returns a link to the documentation for the given filter pattern type
* @param {FilterPatternEnum} type - The type of filter pattern.
* @returns A string
*/
export const getFilterPatternDocsLinks = (type: FilterPatternEnum) => {
switch (type) {
case FilterPatternEnum.DATABASE:
case FilterPatternEnum.SCHEMA:
case FilterPatternEnum.TABLE:
return `https://docs.open-metadata.org/connectors/ingestion/workflows/metadata/filter-patterns/${FilterPatternEnum.DATABASE}#${type}-filter-pattern`;
case FilterPatternEnum.DASHBOARD:
case FilterPatternEnum.CHART:
return 'https://docs.open-metadata.org/connectors/dashboard/metabase#6-configure-metadata-ingestion';
case FilterPatternEnum.TOPIC:
return 'https://docs.open-metadata.org/connectors/messaging/kafka#6-configure-metadata-ingestion';
case FilterPatternEnum.PIPELINE:
return 'https://docs.open-metadata.org/connectors/pipeline/airflow#6-configure-metadata-ingestion';
case FilterPatternEnum.MLMODEL:
return 'https://docs.open-metadata.org/connectors/ml-model/mlflow';
default:
return 'https://docs.open-metadata.org/connectors/ingestion/workflows/metadata/filter-patterns';
}
};