UI : fix request/update task after redirect from frequently joined tables (#17399)

* fix request/update task after redirect from frequently joined tables

* fix cypress test

* modify persona heading like entity header

* change css from fill to stretch and also fix cypress as per comments

* change the breadcrumb name to show display name if available like other entites

* fix advance search cypress

* fix search cypress

* fix unit tests

* Update the logic to fetch the column field for advanced search

* revert permission thing for database

---------

Co-authored-by: Chira Madlani <chirag@getcollate.io>
Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
Co-authored-by: karanh37 <karanh37@gmail.com>
Co-authored-by: Aniket Katkar <aniketkatkar97@gmail.com>
This commit is contained in:
Ashish Gupta 2024-08-15 00:04:48 +05:30 committed by GitHub
parent 75f62a7872
commit fc07324254
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 202 additions and 79 deletions

View File

@ -160,12 +160,14 @@ describe('Persona operations', { tags: 'Settings' }, () => {
verifyResponseStatusCode('@getPersonaDetails', 200);
cy.get(
'[data-testid="page-header-container"] [data-testid="heading"]'
).should('contain', PERSONA_DETAILS.displayName);
cy.get(
'[data-testid="page-header-container"] [data-testid="sub-heading"]'
).should('contain', PERSONA_DETAILS.name);
cy.get('[data-testid="entity-header-name"]').should(
'contain',
PERSONA_DETAILS.name
);
cy.get('[data-testid="entity-header-display-name"]').should(
'contain',
PERSONA_DETAILS.displayName
);
cy.get(
'[data-testid="viewer-container"] [data-testid="markdown-parser"]'
).should('contain', PERSONA_DETAILS.description);
@ -222,11 +224,14 @@ describe('Persona operations', { tags: 'Settings' }, () => {
updatePersonaDisplayName('Test Persona');
cy.get('[data-testid="heading"]').should('contain', 'Test Persona');
cy.get('[data-testid="entity-header-display-name"]').should(
'contain',
'Test Persona'
);
updatePersonaDisplayName(PERSONA_DETAILS.displayName);
cy.get('[data-testid="heading"]').should(
cy.get('[data-testid="entity-header-display-name"]').should(
'contain',
PERSONA_DETAILS.displayName
);

View File

@ -504,7 +504,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => {
interceptURL('GET', `/api/v1/teams/**`, 'getTeams');
interceptURL(
'GET',
`/api/v1/users?fields=teams%2Croles&limit=25&team=${appName}`,
`/api/v1/users?fields=*&limit=25&team=${appName}`,
'teamUsers'
);

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -30,6 +30,7 @@ import { EntityTags, TagFilterOptions } from 'Models';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import {
DE_ACTIVE_COLOR,
ICON_DIMENSION,
@ -110,6 +111,16 @@ const SchemaTable = ({
const [editColumnDisplayName, setEditColumnDisplayName] = useState<Column>();
const { getEntityPermissionByFqn } = usePermissionProvider();
const tableFqn = useMemo(
() =>
getPartialNameFromTableFQN(
decodedEntityFqn,
[FqnPart.Service, FqnPart.Database, FqnPart.Schema, FqnPart.Table],
FQN_SEPARATOR_CHAR
),
[decodedEntityFqn]
);
const sortByOrdinalPosition = useMemo(
() => sortBy(tableColumns, 'ordinalPosition'),
[tableColumns]
@ -143,10 +154,10 @@ const SchemaTable = ({
);
useEffect(() => {
if (!isEmpty(decodedEntityFqn)) {
fetchResourcePermission(decodedEntityFqn);
if (!isEmpty(tableFqn)) {
fetchResourcePermission(tableFqn);
}
}, [decodedEntityFqn]);
}, [tableFqn]);
const handleEditColumn = (column: Column): void => {
setEditColumn(column);
@ -253,7 +264,7 @@ const SchemaTable = ({
field: record.description,
record,
}}
entityFqn={decodedEntityFqn}
entityFqn={tableFqn}
entityType={EntityType.TABLE}
hasEditPermission={hasDescriptionEditAccess}
index={index}
@ -422,7 +433,7 @@ const SchemaTable = ({
),
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
entityFqn={decodedEntityFqn}
entityFqn={tableFqn}
entityType={EntityType.TABLE}
handleTagSelection={handleTagSelection}
hasTagEditAccess={hasTagEditAccess}
@ -454,7 +465,7 @@ const SchemaTable = ({
),
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
entityFqn={decodedEntityFqn}
entityFqn={tableFqn}
entityType={EntityType.TABLE}
handleTagSelection={handleTagSelection}
hasTagEditAccess={hasTagEditAccess}
@ -490,7 +501,7 @@ const SchemaTable = ({
},
],
[
decodedEntityFqn,
tableFqn,
isReadOnly,
tableConstraints,
hasTagEditAccess,

View File

@ -153,7 +153,7 @@ const TagsV1 = ({
{...tagProps}>
{/* Wrap only content to avoid redirect on closeable icons */}
<Link
className="no-underline h-full w-full"
className="no-underline h-full w-max-stretch"
data-testid="tag-redirect-link"
to={redirectLink}>
{tagContent}

View File

@ -143,7 +143,10 @@ jest.mock('react-router-dom', () => ({
.mockImplementation(({ children }: { children: React.ReactNode }) => (
<p data-testid="link">{children}</p>
)),
useHistory: jest.fn(),
useHistory: () => ({
push: jest.fn(),
replace: jest.fn(),
}),
useParams: jest.fn().mockReturnValue({
fqn: 'bigquery.shopify',
}),

View File

@ -144,13 +144,18 @@ jest.mock('../../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(),
}));
jest.mock(
'../../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component',
() => jest.fn().mockImplementation(() => <div>EntityHeaderTitle</div>)
);
describe('PersonaDetailsPage', () => {
it('Component should render', async () => {
render(<PersonaDetailsPage />);
await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
expect(screen.getByTestId('persona-heading')).toBeInTheDocument();
expect(screen.getByText('EntityHeaderTitle')).toBeInTheDocument();
expect(screen.getByText('TitleBreadcrumb.component')).toBeInTheDocument();
expect(
await screen.findByText('ManageButton.component')

View File

@ -10,19 +10,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Col, Row, Tabs, Typography } from 'antd';
import Icon from '@ant-design/icons/lib/components/Icon';
import { Button, Col, Row, Tabs } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isUndefined } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { ReactComponent as IconPersona } from '../../../assets/svg/ic-personas.svg';
import DescriptionV1 from '../../../components/common/EntityDescription/DescriptionV1';
import ManageButton from '../../../components/common/EntityPageInfos/ManageButton/ManageButton';
import NoDataPlaceholder from '../../../components/common/ErrorWithPlaceholder/NoDataPlaceholder';
import Loader from '../../../components/common/Loader/Loader';
import TitleBreadcrumb from '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.component';
import { UserSelectableList } from '../../../components/common/UserSelectableList/UserSelectableList.component';
import EntityHeaderTitle from '../../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component';
import { EntityName } from '../../../components/Modals/EntityNameModal/EntityNameModal.interface';
import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1';
import { UsersTab } from '../../../components/Settings/Users/UsersTab/UsersTabs.component';
@ -182,14 +185,18 @@ export const PersonaDetailsPage = () => {
<Row className="m-b-md page-container" gutter={[0, 16]}>
<Col span={24}>
<div className="d-flex justify-between items-start">
<div>
<div className="w-full">
<TitleBreadcrumb titleLinks={breadcrumb} />
<Typography.Title
className="m-b-0 m-t-xs"
data-testid="persona-heading"
level={5}>
{getEntityName(personaDetails)}
</Typography.Title>
<EntityHeaderTitle
className="m-t-xs"
displayName={personaDetails.displayName}
icon={
<Icon component={IconPersona} style={{ fontSize: '36px' }} />
}
name={personaDetails?.name}
serviceName={personaDetails.name}
/>
</div>
<ManageButton
afterDeleteAction={handleAfterDeleteAction}

View File

@ -140,14 +140,24 @@ const TableDetailsPageV1: React.FC = () => {
);
const [testCaseSummary, setTestCaseSummary] = useState<TestSummary>();
const tableFqn = useMemo(
() =>
getPartialNameFromTableFQN(
datasetFQN,
[FqnPart.Service, FqnPart.Database, FqnPart.Schema, FqnPart.Table],
FQN_SEPARATOR_CHAR
),
[datasetFQN]
);
const extraDropdownContent = useMemo(
() =>
entityUtilClassBase.getManageExtraOptions(
EntityType.TABLE,
datasetFQN,
tableFqn,
tablePermissions
),
[tablePermissions, datasetFQN]
[tablePermissions, tableFqn]
);
const { viewUsagePermission, viewTestCasePermission } = useMemo(
@ -160,16 +170,6 @@ const TableDetailsPageV1: React.FC = () => {
[tablePermissions]
);
const tableFqn = useMemo(
() =>
getPartialNameFromTableFQN(
datasetFQN,
[FqnPart.Service, FqnPart.Database, FqnPart.Schema, FqnPart.Table],
FQN_SEPARATOR_CHAR
),
[datasetFQN]
);
const isViewTableType = useMemo(
() => tableDetails?.tableType === TableType.View,
[tableDetails?.tableType]
@ -560,7 +560,7 @@ const TableDetailsPageV1: React.FC = () => {
<DescriptionV1
showSuggestions
description={tableDetails?.description}
entityFqn={datasetFQN}
entityFqn={tableFqn}
entityName={entityName}
entityType={EntityType.TABLE}
hasEditAccess={editDescriptionPermission}
@ -613,7 +613,7 @@ const TableDetailsPageV1: React.FC = () => {
editCustomAttributePermission
}
editTagPermission={editTagsPermission}
entityFQN={datasetFQN}
entityFQN={tableFqn}
entityId={tableDetails?.id ?? ''}
entityType={EntityType.TABLE}
selectedTags={tableTags}
@ -635,14 +635,22 @@ const TableDetailsPageV1: React.FC = () => {
</Row>
),
[
isTourPage,
tableTags,
joinedTables,
tableFqn,
isEdit,
deleted,
tableDetails,
entityName,
onDescriptionEdit,
onDescriptionUpdate,
testCaseSummary,
editTagsPermission,
editDescriptionPermission,
editAllPermission,
viewAllPermission,
editCustomAttributePermission,
]
);

View File

@ -58,6 +58,9 @@ jest.mock('../../../utils/TasksUtils', () => ({
getBreadCrumbList: jest.fn().mockReturnValue([]),
getTaskMessage: jest.fn().mockReturnValue('Task message'),
getTaskAssignee: jest.fn().mockReturnValue(MOCK_TASK_ASSIGNEE),
getTaskEntityFQN: jest
.fn()
.mockReturnValue('sample_data.ecommerce_db.shopify.dim_location'),
}));
jest.mock('../shared/Assignees', () =>
jest.fn().mockImplementation(() => <div>Assignees.component</div>)

View File

@ -47,6 +47,7 @@ import {
fetchOptions,
getBreadCrumbList,
getTaskAssignee,
getTaskEntityFQN,
getTaskMessage,
} from '../../../utils/TasksUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
@ -64,7 +65,7 @@ const RequestDescription = () => {
const { entityType } = useParams<{ entityType: EntityType }>();
const { fqn: decodedEntityFQN } = useFqn();
const { fqn } = useFqn();
const queryParams = new URLSearchParams(location.search);
const field = queryParams.get('field');
@ -76,6 +77,11 @@ const RequestDescription = () => {
const [suggestion, setSuggestion] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const entityFQN = useMemo(
() => getTaskEntityFQN(entityType, fqn),
[fqn, entityType]
);
const taskMessage = useMemo(
() =>
getTaskMessage({
@ -116,7 +122,7 @@ const RequestDescription = () => {
const data: CreateThread = {
from: currentUser?.name as string,
message: value.title || taskMessage,
about: getEntityFeedLink(entityType, decodedEntityFQN, getTaskAbout()),
about: getEntityFeedLink(entityType, entityFQN, getTaskAbout()),
taskDetails: {
assignees: assignees.map((assignee) => ({
id: assignee.value,
@ -138,7 +144,7 @@ const RequestDescription = () => {
history.push(
entityUtilClassBase.getEntityLink(
entityType,
decodedEntityFQN,
entityFQN,
EntityTabs.ACTIVITY_FEED,
ActivityFeedTabs.TASKS
)
@ -152,8 +158,8 @@ const RequestDescription = () => {
};
useEffect(() => {
fetchEntityDetail(entityType, decodedEntityFQN, setEntityData);
}, [decodedEntityFQN, entityType]);
fetchEntityDetail(entityType, entityFQN, setEntityData);
}, [entityFQN, entityType]);
useEffect(() => {
const defaultAssignee = getTaskAssignee(entityData as Glossary);

View File

@ -59,6 +59,9 @@ jest.mock('../../../utils/TasksUtils', () => ({
getBreadCrumbList: jest.fn().mockReturnValue([]),
getTaskMessage: jest.fn().mockReturnValue('Task message'),
getTaskAssignee: jest.fn().mockReturnValue(MOCK_TASK_ASSIGNEE),
getTaskEntityFQN: jest
.fn()
.mockReturnValue('sample_data.ecommerce_db.shopify.dim_location'),
}));
jest.mock('../shared/Assignees', () =>
jest.fn().mockImplementation(() => <div>Assignees.component</div>)

View File

@ -46,6 +46,7 @@ import {
fetchOptions,
getBreadCrumbList,
getTaskAssignee,
getTaskEntityFQN,
getTaskMessage,
} from '../../../utils/TasksUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
@ -61,7 +62,7 @@ const RequestTag = () => {
const history = useHistory();
const [form] = useForm();
const { entityType } = useParams<{ entityType: EntityType }>();
const { fqn: entityFQN } = useFqn();
const { fqn } = useFqn();
const queryParams = new URLSearchParams(location.search);
const field = queryParams.get('field');
@ -73,6 +74,11 @@ const RequestTag = () => {
const [suggestion] = useState<TagLabel[]>([]);
const [isLoading, setIsLoading] = useState(false);
const entityFQN = useMemo(
() => getTaskEntityFQN(entityType, fqn),
[fqn, entityType]
);
const taskMessage = useMemo(
() =>
getTaskMessage({

View File

@ -80,6 +80,9 @@ jest.mock('../../../utils/TasksUtils', () => ({
description: mockTableData.columns[0].description,
})),
getTaskAssignee: jest.fn().mockReturnValue(MOCK_TASK_ASSIGNEE),
getTaskEntityFQN: jest
.fn()
.mockReturnValue('sample_data.ecommerce_db.shopify.dim_location'),
}));
jest.mock('../shared/Assignees', () =>
jest.fn().mockImplementation(() => <div>Assignees.component</div>)

View File

@ -50,6 +50,7 @@ import {
getColumnObject,
getEntityColumnsDetails,
getTaskAssignee,
getTaskEntityFQN,
getTaskMessage,
} from '../../../utils/TasksUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
@ -66,7 +67,7 @@ const UpdateDescription = () => {
const [form] = useForm();
const { entityType } = useParams<{ entityType: EntityType }>();
const { fqn: entityFQN } = useFqn();
const { fqn } = useFqn();
const queryParams = new URLSearchParams(location.search);
const field = queryParams.get('field');
@ -78,6 +79,11 @@ const UpdateDescription = () => {
const [currentDescription, setCurrentDescription] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const entityFQN = useMemo(
() => getTaskEntityFQN(entityType, fqn),
[fqn, entityType]
);
const sanitizeValue = useMemo(
() => value?.replaceAll(TASK_SANITIZE_VALUE_REGEX, '') ?? '',
[value]

View File

@ -90,6 +90,9 @@ jest.mock('../../../utils/TasksUtils', () => ({
getColumnObject: jest.fn().mockImplementation(() => ({
tags: mockTableData.columns[0].tags,
})),
getTaskEntityFQN: jest
.fn()
.mockReturnValue('sample_data.ecommerce_db.shopify.dim_location'),
getTaskAssignee: jest.fn().mockReturnValue(MOCK_TASK_ASSIGNEE),
}));
jest.mock('../shared/Assignees', () =>

View File

@ -51,6 +51,7 @@ import {
getColumnObject,
getEntityColumnsDetails,
getTaskAssignee,
getTaskEntityFQN,
getTaskMessage,
} from '../../../utils/TasksUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
@ -68,7 +69,7 @@ const UpdateTag = () => {
const { entityType } = useParams<{ entityType: EntityType }>();
const { fqn: entityFQN } = useFqn();
const { fqn } = useFqn();
const queryParams = new URLSearchParams(location.search);
const field = queryParams.get('field');
@ -83,6 +84,11 @@ const UpdateTag = () => {
const [suggestion, setSuggestion] = useState<TagLabel[]>([]);
const [isLoading, setIsLoading] = useState(false);
const entityFQN = useMemo(
() => getTaskEntityFQN(entityType, fqn),
[fqn, entityType]
);
const sanitizeValue = useMemo(
() => value?.replaceAll(TASK_SANITIZE_VALUE_REGEX, '') ?? '',
[value]

View File

@ -174,6 +174,9 @@
.w-max-full-140 {
max-width: calc(100% - 140px);
}
.w-max-stretch {
max-width: stretch;
}
.w-auto {
width: auto;

View File

@ -12,7 +12,7 @@
*/
import { t } from 'i18next';
import { sortBy } from 'lodash';
import { isEmpty, sortBy } from 'lodash';
import {
AsyncFetchListValues,
AsyncFetchListValuesResult,
@ -97,19 +97,6 @@ class AdvancedSearchClassBase {
},
},
'columns.name.keyword': {
label: t('label.column'),
type: 'select',
mainWidgetProps: this.mainWidgetProps,
fieldSettings: {
asyncFetch: this.autocomplete({
searchIndex: SearchIndex.TABLE,
entityField: EntityFields.COLUMN,
}),
useAsyncSearch: true,
},
},
tableType: {
label: t('label.table-type'),
type: 'select',
@ -320,18 +307,6 @@ class AdvancedSearchClassBase {
useAsyncSearch: true,
},
},
'columns.name.keyword': {
label: t('label.data-model-column'),
type: 'select',
mainWidgetProps: this.mainWidgetProps,
fieldSettings: {
asyncFetch: this.autocomplete({
searchIndex: SearchIndex.DASHBOARD_DATA_MODEL,
entityField: EntityFields.COLUMN,
}),
useAsyncSearch: true,
},
},
'project.keyword': {
label: t('label.project'),
type: 'select',
@ -551,6 +526,36 @@ class AdvancedSearchClassBase {
};
}
// Since the column field key 'columns.name.keyword` is common in table and data model,
// Following function is used to get the column field config based on the search index
// or if it is an explore page
public getColumnConfig = (entitySearchIndex: SearchIndex[]) => {
const searchIndexWithColumns = entitySearchIndex.filter(
(index) =>
index === SearchIndex.TABLE ||
index === SearchIndex.DASHBOARD_DATA_MODEL ||
index === SearchIndex.DATA_ASSET ||
index === SearchIndex.ALL
);
return !isEmpty(searchIndexWithColumns)
? {
'columns.name.keyword': {
label: t('label.column'),
type: 'select',
mainWidgetProps: this.mainWidgetProps,
fieldSettings: {
asyncFetch: this.autocomplete({
searchIndex: searchIndexWithColumns,
entityField: EntityFields.COLUMN,
}),
useAsyncSearch: true,
},
},
}
: {};
};
/**
* Get entity specific fields for the query builder
*/
@ -631,6 +636,7 @@ class AdvancedSearchClassBase {
...this.getCommonConfig({ entitySearchIndex, tierOptions }),
...(shouldAddServiceField ? serviceQueryBuilderFields : {}),
...this.getEntitySpecificQueryBuilderFields(entitySearchIndex),
...this.getColumnConfig(entitySearchIndex),
};
// Sort the fields according to the label
@ -648,7 +654,7 @@ class AdvancedSearchClassBase {
isExplorePage?: boolean
) => BasicConfig = (tierOptions, entitySearchIndex, isExplorePage) => {
const searchIndexWithServices = [
SearchIndex.ALL,
SearchIndex.DATA_ASSET,
SearchIndex.TABLE,
SearchIndex.DASHBOARD,
SearchIndex.PIPELINE,

View File

@ -21,6 +21,7 @@ import {
fetchOptions,
getEntityTableName,
getTaskAssignee,
getTaskEntityFQN,
getTaskMessage,
} from './TasksUtils';
@ -298,3 +299,28 @@ describe('Tests for getTaskAssignee', () => {
]);
});
});
describe('Tests for getTaskEntityFQN', () => {
it('should return fqn for table entity', async () => {
const fqn = 'sample_data.ecommerce_db.shopify."dim.product"';
const response = getTaskEntityFQN(EntityType.TABLE, fqn);
expect(response).toEqual(fqn);
});
it('should return table fqn only when column name present in fqn', async () => {
const response = getTaskEntityFQN(
EntityType.TABLE,
'sample_data.ecommerce_db.shopify."dim.product".address_id'
);
expect(response).toEqual('sample_data.ecommerce_db.shopify."dim.product"');
});
it('should return fqn as it is if entity type is not table', async () => {
const fqn = 'sample_looker.customers';
const response = getTaskEntityFQN(EntityType.DASHBOARD, fqn);
expect(response).toEqual(fqn);
});
});

View File

@ -19,6 +19,7 @@ import { ReactComponent as CancelColored } from '../assets/svg/cancel-colored.sv
import { ReactComponent as EditColored } from '../assets/svg/edit-colored.svg';
import { ReactComponent as SuccessColored } from '../assets/svg/success-colored.svg';
import { ActivityFeedTabs } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import {
getEntityDetailsPath,
getGlossaryTermDetailsPath,
@ -901,3 +902,15 @@ export const getTaskAssignee = (entityData: Glossary): Option[] => {
return defaultAssignee;
};
export const getTaskEntityFQN = (entityType: EntityType, fqn: string) => {
if (entityType === EntityType.TABLE) {
return getPartialNameFromTableFQN(
fqn,
[FqnPart.Service, FqnPart.Database, FqnPart.Schema, FqnPart.Table],
FQN_SEPARATOR_CHAR
);
}
return fqn;
};