Added Support tasks for all entity (#12289)

* working on support task for all entites

* added support task for topic entity

* added support task for all entities

* added ui support for entity task of all the entities

* solve issue for container and topic tasks

* fix the task issue and ui improvements for tag icons

* added support for tasks assigned to children

* code improvement

* ui improvement around tags for icons

* changes as per comments

* fix unit test

---------

Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
07Himank 2023-07-13 11:35:05 +05:30 committed by GitHub
parent e698589d55
commit 36f8cf6900
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1333 additions and 1043 deletions

View File

@ -13,14 +13,24 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.json.JsonPatch;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.entity.data.Container;
import org.openmetadata.schema.entity.services.StorageService;
import org.openmetadata.schema.type.*;
import org.openmetadata.schema.type.Column;
import org.openmetadata.schema.type.ContainerFileFormat;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.type.TaskDetails;
import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.resources.feeds.MessageParser;
import org.openmetadata.service.resources.storages.ContainerResource;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.FullyQualifiedName;
import org.openmetadata.service.util.JsonUtils;
public class ContainerRepository extends EntityRepository<Container> {
@ -214,6 +224,36 @@ public class ContainerRepository extends EntityRepository<Container> {
return allTags;
}
@Override
public void update(TaskDetails task, MessageParser.EntityLink entityLink, String newValue, String user)
throws IOException {
// TODO move this as the first check
if (entityLink.getFieldName().equals("dataModel")) {
Container container = getByName(null, entityLink.getEntityFQN(), getFields("dataModel,tags"), Include.ALL);
Column column =
container.getDataModel().getColumns().stream()
.filter(c -> c.getName().equals(entityLink.getArrayFieldName()))
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
CatalogExceptionMessage.invalidFieldName("column", entityLink.getArrayFieldName())));
String origJson = JsonUtils.pojoToJson(container);
if (EntityUtil.isDescriptionTask(task.getType())) {
column.setDescription(newValue);
} else if (EntityUtil.isTagTask(task.getType())) {
List<TagLabel> tags = JsonUtils.readObjects(newValue, TagLabel.class);
column.setTags(tags);
}
String updatedEntityJson = JsonUtils.pojoToJson(container);
JsonPatch patch = JsonUtils.getJsonPatch(origJson, updatedEntityJson);
patch(null, container.getId(), user, patch);
return;
}
super.update(task, entityLink, newValue, user);
}
private void addDerivedColumnTags(List<Column> columns) {
if (nullOrEmpty(columns)) {
return;

View File

@ -26,6 +26,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.json.JsonPatch;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.entity.data.MlModel;
@ -37,11 +38,15 @@ import org.openmetadata.schema.type.MlFeatureSource;
import org.openmetadata.schema.type.MlHyperParameter;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.type.TaskDetails;
import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.resources.feeds.MessageParser;
import org.openmetadata.service.resources.mlmodels.MlModelResource;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.FullyQualifiedName;
import org.openmetadata.service.util.JsonUtils;
@Slf4j
public class MlModelRepository extends EntityRepository<MlModel> {
@ -221,6 +226,35 @@ public class MlModelRepository extends EntityRepository<MlModel> {
return allTags;
}
@Override
public void update(TaskDetails task, MessageParser.EntityLink entityLink, String newValue, String user)
throws IOException {
if (entityLink.getFieldName().equals("mlFeatures")) {
MlModel mlModel = getByName(null, entityLink.getEntityFQN(), getFields("tags"), Include.ALL);
MlFeature mlFeature =
mlModel.getMlFeatures().stream()
.filter(c -> c.getName().equals(entityLink.getArrayFieldName()))
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
CatalogExceptionMessage.invalidFieldName("chart", entityLink.getArrayFieldName())));
String origJson = JsonUtils.pojoToJson(mlModel);
if (EntityUtil.isDescriptionTask(task.getType())) {
mlFeature.setDescription(newValue);
} else if (EntityUtil.isTagTask(task.getType())) {
List<TagLabel> tags = JsonUtils.readObjects(newValue, TagLabel.class);
mlFeature.setTags(tags);
}
String updatedEntityJson = JsonUtils.pojoToJson(mlModel);
JsonPatch patch = JsonUtils.getJsonPatch(origJson, updatedEntityJson);
patch(null, mlModel.getId(), user, patch);
return;
}
super.update(task, entityLink, newValue, user);
}
private void populateService(MlModel mlModel) throws IOException {
MlModelService service = Entity.getEntity(mlModel.getService(), "", Include.NON_DELETED);
mlModel.setService(service.getEntityReference());

View File

@ -695,16 +695,29 @@ public class TableRepository extends EntityRepository<Table> {
public void update(TaskDetails task, EntityLink entityLink, String newValue, String user) throws IOException {
validateEntityLinkFieldExists(entityLink, task.getType());
if (entityLink.getFieldName().equals("columns")) {
String columnName = entityLink.getArrayFieldName();
String childrenName = "";
if (entityLink.getArrayFieldName().contains(".")) {
String fieldNameWithoutQuotes =
entityLink.getArrayFieldName().substring(1, entityLink.getArrayFieldName().length() - 1);
columnName = fieldNameWithoutQuotes.substring(0, fieldNameWithoutQuotes.indexOf("."));
childrenName = fieldNameWithoutQuotes.substring(fieldNameWithoutQuotes.lastIndexOf(".") + 1);
}
Table table = getByName(null, entityLink.getEntityFQN(), getFields("columns,tags"), Include.ALL);
Column column =
table.getColumns().stream()
.filter(c -> c.getName().equals(entityLink.getArrayFieldName()))
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
CatalogExceptionMessage.invalidFieldName("column", entityLink.getArrayFieldName())));
Column column = null;
for (Column c : table.getColumns()) {
if (c.getName().equals(columnName)) {
column = c;
break;
}
}
if (childrenName != "" && column != null) {
column = getChildrenColumn(column.getChildren(), childrenName);
}
if (column == null) {
throw new IllegalArgumentException(
CatalogExceptionMessage.invalidFieldName("column", entityLink.getArrayFieldName()));
}
String origJson = JsonUtils.pojoToJson(table);
if (EntityUtil.isDescriptionTask(task.getType())) {
column.setDescription(newValue);
@ -720,6 +733,27 @@ public class TableRepository extends EntityRepository<Table> {
super.update(task, entityLink, newValue, user);
}
private static Column getChildrenColumn(List<Column> column, String childrenName) {
Column childrenColumn = null;
for (Column col : column) {
if (col.getName().equals(childrenName)) {
childrenColumn = col;
break;
}
}
if (childrenColumn == null) {
for (int i = 0; i < column.size(); i++) {
if (column.get(i).getChildren() != null) {
childrenColumn = getChildrenColumn(column.get(i).getChildren(), childrenName);
if (childrenColumn != null) {
break;
}
}
}
}
return childrenColumn;
}
private void getColumnTags(boolean setTags, List<Column> columns) {
for (Column c : listOrEmpty(columns)) {
c.setTags(setTags ? getTags(c.getFullyQualifiedName()) : null);

View File

@ -15,7 +15,10 @@ package org.openmetadata.service.jdbi3;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.service.Entity.*;
import static org.openmetadata.service.Entity.FIELD_DESCRIPTION;
import static org.openmetadata.service.Entity.FIELD_DISPLAY_NAME;
import static org.openmetadata.service.Entity.FIELD_FOLLOWERS;
import static org.openmetadata.service.Entity.FIELD_TAGS;
import static org.openmetadata.service.util.EntityUtil.getSchemaField;
import com.fasterxml.jackson.core.JsonProcessingException;
@ -29,6 +32,7 @@ import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.json.JsonPatch;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.entity.data.Topic;
@ -38,9 +42,12 @@ import org.openmetadata.schema.type.Field;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.schema.type.TaskDetails;
import org.openmetadata.schema.type.topic.CleanupPolicy;
import org.openmetadata.schema.type.topic.TopicSampleData;
import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.CatalogExceptionMessage;
import org.openmetadata.service.resources.feeds.MessageParser;
import org.openmetadata.service.resources.topics.TopicResource;
import org.openmetadata.service.security.mask.PIIMasker;
import org.openmetadata.service.util.EntityUtil;
@ -264,6 +271,70 @@ public class TopicRepository extends EntityRepository<Topic> {
return allTags;
}
@Override
public void update(TaskDetails task, MessageParser.EntityLink entityLink, String newValue, String user)
throws IOException {
if (entityLink.getFieldName().equals("messageSchema")) {
String schemaName = entityLink.getArrayFieldName();
String childrenSchemaName = "";
if (entityLink.getArrayFieldName().contains(".")) {
String fieldNameWithoutQuotes =
entityLink.getArrayFieldName().substring(1, entityLink.getArrayFieldName().length() - 1);
schemaName = fieldNameWithoutQuotes.substring(0, fieldNameWithoutQuotes.indexOf("."));
childrenSchemaName = fieldNameWithoutQuotes.substring(fieldNameWithoutQuotes.lastIndexOf(".") + 1);
}
Topic topic = getByName(null, entityLink.getEntityFQN(), getFields("tags"), Include.ALL);
Field schemaField = null;
for (Field field : topic.getMessageSchema().getSchemaFields()) {
if (field.getName().equals(schemaName)) {
schemaField = field;
break;
}
}
if (childrenSchemaName != "" && schemaField != null) {
schemaField = getchildrenSchemaField(schemaField.getChildren(), childrenSchemaName);
}
if (schemaField == null) {
throw new IllegalArgumentException(
CatalogExceptionMessage.invalidFieldName("schema", entityLink.getArrayFieldName()));
}
String origJson = JsonUtils.pojoToJson(topic);
if (EntityUtil.isDescriptionTask(task.getType())) {
schemaField.setDescription(newValue);
} else if (EntityUtil.isTagTask(task.getType())) {
List<TagLabel> tags = JsonUtils.readObjects(newValue, TagLabel.class);
schemaField.setTags(tags);
}
String updatedEntityJson = JsonUtils.pojoToJson(topic);
JsonPatch patch = JsonUtils.getJsonPatch(origJson, updatedEntityJson);
patch(null, topic.getId(), user, patch);
return;
}
super.update(task, entityLink, newValue, user);
}
private static Field getchildrenSchemaField(List<Field> fields, String childrenSchemaName) {
Field childrenSchemaField = null;
for (Field field : fields) {
if (field.getName().equals(childrenSchemaName)) {
childrenSchemaField = field;
break;
}
}
if (childrenSchemaField == null) {
for (int i = 0; i < fields.size(); i++) {
if (fields.get(i).getChildren() != null) {
childrenSchemaField = getchildrenSchemaField(fields.get(i).getChildren(), childrenSchemaName);
if (childrenSchemaField != null) {
break;
}
}
}
}
return childrenSchemaField;
}
public static Set<TagLabel> getAllFieldTags(Field field) {
Set<TagLabel> tags = new HashSet<>();
if (!listOrEmpty(field.getTags()).isEmpty()) {

View File

@ -10,7 +10,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ThreadType } from 'generated/api/feed/createThread';
import { Container } from 'generated/entity/data/container';
import { EntityFieldThreads } from 'interface/feed.interface';
import { ReactNode } from 'react';
export type CellRendered<T, K extends keyof T> = (
@ -24,5 +26,8 @@ export interface ContainerDataModelProps {
hasDescriptionEditAccess: boolean;
hasTagEditAccess: boolean;
isReadOnly: boolean;
entityFqn: string;
entityFieldThreads: EntityFieldThreads[];
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
onUpdate: (updatedDataModel: Container['dataModel']) => Promise<void>;
}

View File

@ -73,6 +73,16 @@ const props = {
hasTagEditAccess: true,
isReadOnly: false,
onUpdate: jest.fn(),
entityFqn: 's3_storage_sample.departments',
entityFieldThreads: [
{
entityLink:
'<#E::container::s3_storage_sample.departments.finance.expenditures::dataModel::columns::department_id::description>',
count: 2,
entityField: 'dataModel::columns::department_id::description',
},
],
onThreadLinkSelect: jest.fn(),
};
jest.mock('utils/TagsUtils', () => ({

View File

@ -10,14 +10,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Popover, Space, Typography } from 'antd';
import { Popover, Typography } from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import { ModalWithMarkdownEditor } from 'components/Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import TableDescription from 'components/TableDescription/TableDescription.component';
import TableTags from 'components/TableTags/TableTags.component';
import { TABLE_SCROLL_VALUE } from 'constants/Table.constants';
import { EntityType } from 'enums/entity.enum';
import { Column, TagLabel } from 'generated/entity/data/container';
import { TagSource } from 'generated/type/tagLabel';
import { cloneDeep, isEmpty, isUndefined, map, toLower } from 'lodash';
@ -30,10 +30,7 @@ import {
} from 'utils/ContainerDetailUtils';
import { getEntityName } from 'utils/EntityUtils';
import { getTableExpandableConfig } from 'utils/TableUtils';
import {
CellRendered,
ContainerDataModelProps,
} from './ContainerDataModel.interface';
import { ContainerDataModelProps } from './ContainerDataModel.interface';
const ContainerDataModel: FC<ContainerDataModelProps> = ({
dataModel,
@ -41,6 +38,9 @@ const ContainerDataModel: FC<ContainerDataModelProps> = ({
hasTagEditAccess,
isReadOnly,
onUpdate,
entityFqn,
entityFieldThreads,
onThreadLinkSelect,
}) => {
const { t } = useTranslation();
@ -84,38 +84,6 @@ const ContainerDataModel: FC<ContainerDataModelProps> = ({
setEditContainerColumnDescription(undefined);
};
const renderContainerColumnDescription: CellRendered<Column, 'description'> =
(description, record, index) => {
return (
<Space
className="custom-group w-full"
data-testid="description"
id={`field-description-${index}`}
size={4}>
<>
{description ? (
<RichTextEditorPreviewer markdown={description} />
) : (
<Typography.Text className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</Typography.Text>
)}
</>
{isReadOnly || !hasDescriptionEditAccess ? null : (
<Button
className="p-0 opacity-0 group-hover-opacity-100 flex-center"
data-testid="edit-button"
icon={<EditIcon width="16px" />}
type="text"
onClick={() => setEditContainerColumnDescription(record)}
/>
)}
</Space>
);
};
const columns: ColumnsType<Column> = useMemo(
() => [
{
@ -168,7 +136,22 @@ const ContainerDataModel: FC<ContainerDataModelProps> = ({
key: 'description',
accessor: 'description',
width: 350,
render: renderContainerColumnDescription,
render: (_, record, index) => (
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.CONTAINER}
hasEditPermission={hasDescriptionEditAccess}
index={index}
isReadOnly={isReadOnly}
onClick={() => setEditContainerColumnDescription(record)}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
{
title: t('label.tag-plural'),
@ -178,6 +161,9 @@ const ContainerDataModel: FC<ContainerDataModelProps> = ({
width: 300,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.CONTAINER}
handleTagSelection={handleFieldTagsChange}
hasTagEditAccess={hasTagEditAccess}
index={index}
@ -185,6 +171,7 @@ const ContainerDataModel: FC<ContainerDataModelProps> = ({
record={record}
tags={tags}
type={TagSource.Classification}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
@ -196,6 +183,9 @@ const ContainerDataModel: FC<ContainerDataModelProps> = ({
width: 300,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.CONTAINER}
handleTagSelection={handleFieldTagsChange}
hasTagEditAccess={hasTagEditAccess}
index={index}
@ -203,15 +193,20 @@ const ContainerDataModel: FC<ContainerDataModelProps> = ({
record={record}
tags={tags}
type={TagSource.Glossary}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
],
[
isReadOnly,
entityFqn,
hasTagEditAccess,
entityFieldThreads,
hasDescriptionEditAccess,
editContainerColumnDescription,
getEntityName,
onThreadLinkSelect,
handleFieldTagsChange,
]
);

View File

@ -11,9 +11,8 @@
* limitations under the License.
*/
import { Card, Col, Row, Space, Table, Tabs, Tooltip, Typography } from 'antd';
import { Card, Col, Row, Space, Table, Tabs, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { AxiosError } from 'axios';
import ActivityFeedProvider, {
useActivityFeedProvider,
@ -57,7 +56,6 @@ import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
import { CustomPropertyTable } from '../common/CustomPropertyTable/CustomPropertyTable';
import { CustomPropertyProps } from '../common/CustomPropertyTable/CustomPropertyTable.interface';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import EntityLineageComponent from '../EntityLineage/EntityLineage.component';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
@ -68,11 +66,14 @@ import {
DashboardDetailsProps,
} from './DashboardDetails.interface';
import TableDescription from 'components/TableDescription/TableDescription.component';
const DashboardDetails = ({
charts,
dashboardDetails,
fetchDashboard,
followDashboardHandler,
unFollowDashboardHandler,
dashboardDetails,
charts,
chartDescriptionUpdateHandler,
chartTagUpdateHandler,
versionHandler,
@ -94,10 +95,6 @@ const DashboardDetails = ({
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
const [threadLink, setThreadLink] = useState<string>('');
const [threadType, setThreadType] = useState<ThreadType>(
@ -118,14 +115,16 @@ const DashboardDetails = ({
deleted,
dashboardTags,
tier,
entityFqn,
} = useMemo(() => {
const { tags = [] } = dashboardDetails;
const { tags = [], fullyQualifiedName } = dashboardDetails;
return {
...dashboardDetails,
tier: getTierTags(tags),
dashboardTags: getTagsWithoutTier(tags),
entityName: getEntityName(dashboardDetails),
entityFqn: fullyQualifiedName ?? '',
};
}, [dashboardDetails]);
@ -177,7 +176,6 @@ const DashboardDetails = ({
EntityType.DASHBOARD,
dashboardFQN,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
@ -390,54 +388,6 @@ const DashboardDetails = ({
setThreadLink('');
};
const renderDescription = useCallback(
(text, record, index) => {
const permissionsObject = chartsPermissionsArray?.find(
(chart) => chart.id === record.id
)?.permissions;
const editDescriptionPermissions =
!isUndefined(permissionsObject) &&
(permissionsObject.EditDescription || permissionsObject.EditAll);
return (
<Space
className="w-full tw-group cursor-pointer"
data-testid="description">
<div>
{text ? (
<RichTextEditorPreviewer markdown={text} />
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</span>
)}
</div>
{!deleted && (
<Tooltip
title={
editDescriptionPermissions
? t('label.edit-entity', {
entity: t('label.description'),
})
: t('message.no-permission-for-action')
}>
<button
className="tw-self-start tw-w-8 tw-h-auto tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none"
disabled={!editDescriptionPermissions}
onClick={() => handleUpdateChart(record, index)}>
<EditIcon width={16} />
</button>
</Tooltip>
)}
</Space>
);
},
[chartsPermissionsArray, handleUpdateChart]
);
const hasEditTagAccess = (record: ChartType) => {
const permissionsObject = chartsPermissionsArray?.find(
(chart) => chart.id === record.id
@ -464,6 +414,12 @@ const DashboardDetails = ({
}
};
const entityFieldThreads = useMemo(
() =>
getEntityFieldThreadCounts(EntityField.CHARTS, entityFieldThreadCount),
[entityFieldThreadCount, getEntityFieldThreadCounts]
);
const tableColumn: ColumnsType<ChartType> = useMemo(
() => [
{
@ -501,7 +457,35 @@ const DashboardDetails = ({
dataIndex: 'description',
key: 'description',
width: 350,
render: renderDescription,
render: (_, record, index) => {
const permissionsObject = chartsPermissionsArray?.find(
(chart) => chart.id === record.id
)?.permissions;
const editDescriptionPermissions =
!isUndefined(permissionsObject) &&
(permissionsObject.EditDescription || permissionsObject.EditAll);
return (
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
}}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldThreadCount
)}
entityFqn={entityFqn}
entityType={EntityType.DASHBOARD}
hasEditPermission={editDescriptionPermissions}
index={index}
isReadOnly={deleted}
onClick={() => handleUpdateChart(record, index)}
onThreadLinkSelect={onThreadLinkSelect}
/>
);
},
},
{
title: t('label.tag-plural'),
@ -512,6 +496,9 @@ const DashboardDetails = ({
render: (tags: TagLabel[], record: ChartType, index: number) => {
return (
<TableTags<ChartType>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.DASHBOARD}
handleTagSelection={handleChartTagSelection}
hasTagEditAccess={hasEditTagAccess(record)}
index={index}
@ -519,6 +506,7 @@ const DashboardDetails = ({
record={record}
tags={tags}
type={TagSource.Classification}
onThreadLinkSelect={onThreadLinkSelect}
/>
);
},
@ -531,6 +519,9 @@ const DashboardDetails = ({
width: 300,
render: (tags: TagLabel[], record: ChartType, index: number) => (
<TableTags<ChartType>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.DASHBOARD}
handleTagSelection={handleChartTagSelection}
hasTagEditAccess={hasEditTagAccess(record)}
index={index}
@ -538,11 +529,23 @@ const DashboardDetails = ({
record={record}
tags={tags}
type={TagSource.Glossary}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
],
[deleted, renderDescription, handleChartTagSelection, hasEditTagAccess]
[
deleted,
entityFqn,
entityFieldThreads,
entityFieldThreadCount,
chartsPermissionsArray,
onThreadLinkSelect,
hasEditTagAccess,
handleUpdateChart,
handleChartTagSelection,
getEntityFieldThreadCounts,
]
);
const tabs = useMemo(
@ -649,6 +652,7 @@ const DashboardDetails = ({
entityType={EntityType.DASHBOARD}
fqn={dashboardDetails?.fullyQualifiedName ?? ''}
onFeedUpdate={getEntityFeedCount}
onUpdateEntityDetails={fetchDashboard}
/>
</ActivityFeedProvider>
),
@ -699,7 +703,6 @@ const DashboardDetails = ({
tableColumn,
dashboardDetails,
charts,
entityFieldTaskCount,
entityFieldThreadCount,
entityName,
dashboardPermissions,

View File

@ -28,6 +28,7 @@ export interface ChartsPermissions {
export interface DashboardDetailsProps {
charts: Array<ChartType>;
dashboardDetails: Dashboard;
fetchDashboard: () => void;
createThread: (data: CreateThread) => void;
followDashboardHandler: () => Promise<void>;
unFollowDashboardHandler: () => Promise<void>;

View File

@ -66,6 +66,7 @@ const dashboardDetailsProps: DashboardDetailsProps = {
onDashboardUpdate: jest.fn(),
versionHandler: jest.fn(),
createThread: jest.fn(),
fetchDashboard: jest.fn(),
};
const mockEntityPermissions = {

View File

@ -31,7 +31,7 @@ import { CSMode } from 'enums/codemirror.enum';
import { EntityTabs, EntityType } from 'enums/entity.enum';
import { LabelType, State, TagLabel, TagSource } from 'generated/type/tagLabel';
import { EntityFieldThreadCount } from 'interface/feed.interface';
import { isUndefined, noop, toString } from 'lodash';
import { isUndefined, toString } from 'lodash';
import { EntityTags } from 'Models';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -46,6 +46,7 @@ import ModelTab from './ModelTab/ModelTab.component';
const DataModelDetails = ({
dataModelData,
dataModelPermissions,
fetchDataModel,
createThread,
handleFollowDataModel,
handleUpdateTags,
@ -100,7 +101,6 @@ const DataModelDetails = ({
EntityType.DASHBOARD_DATA_MODEL,
dashboardDataModelFQN,
setEntityFieldThreadCount,
noop,
setFeedCount
);
};
@ -183,9 +183,15 @@ const DataModelDetails = ({
/>
<ModelTab
data={dataModelData?.columns || []}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.COLUMNS,
entityFieldThreadCount
)}
entityFqn={dashboardDataModelFQN}
hasEditDescriptionPermission={hasEditDescriptionPermission}
hasEditTagsPermission={hasEditTagsPermission}
isReadOnly={Boolean(deleted)}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={handleColumnUpdateDataModel}
/>
</div>
@ -231,7 +237,6 @@ const DataModelDetails = ({
entityName,
handleTagSelection,
onThreadLinkSelect,
onThreadLinkSelect,
handleColumnUpdateDataModel,
handleUpdateDescription,
getEntityFieldThreadCounts,
@ -266,6 +271,7 @@ const DataModelDetails = ({
entityType={EntityType.DASHBOARD_DATA_MODEL}
fqn={dataModelData?.fullyQualifiedName ?? ''}
onFeedUpdate={getEntityFeedCount}
onUpdateEntityDetails={fetchDataModel}
/>
</ActivityFeedProvider>
),

View File

@ -21,6 +21,7 @@ import { EntityTags } from 'Models';
export interface DataModelDetailsProps {
dataModelData: DashboardDataModel;
dataModelPermissions: OperationPermission;
fetchDataModel: () => void;
createThread: (data: CreateThread) => void;
handleFollowDataModel: () => Promise<void>;
handleUpdateTags: (selectedTags?: EntityTags[]) => void;

View File

@ -10,13 +10,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Space, Table, Typography } from 'antd';
import { Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import { CellRendered } from 'components/ContainerDetail/ContainerDataModel/ContainerDataModel.interface';
import { ModalWithMarkdownEditor } from 'components/Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import TableDescription from 'components/TableDescription/TableDescription.component';
import TableTags from 'components/TableTags/TableTags.component';
import { EntityType } from 'enums/entity.enum';
import { Column } from 'generated/entity/data/dashboardDataModel';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
import { cloneDeep, isUndefined, map } from 'lodash';
@ -36,6 +35,9 @@ const ModelTab = ({
hasEditDescriptionPermission,
hasEditTagsPermission,
onUpdate,
entityFqn,
entityFieldThreads,
onThreadLinkSelect,
}: ModelTabProps) => {
const { t } = useTranslation();
const [editColumnDescription, setEditColumnDescription] = useState<Column>();
@ -76,41 +78,6 @@ const ModelTab = ({
[editColumnDescription, data]
);
const renderColumnDescription: CellRendered<Column, 'description'> =
useCallback(
(description, record, index) => {
return (
<Space
className="custom-group w-full"
data-testid="description"
id={`field-description-${index}`}
size={4}>
<>
{description ? (
<RichTextEditorPreviewer markdown={description} />
) : (
<Typography.Text className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</Typography.Text>
)}
</>
{isReadOnly && !hasEditDescriptionPermission ? null : (
<Button
className="p-0 opacity-0 group-hover-opacity-100"
data-testid="edit-button"
icon={<EditIcon width="16px" />}
type="text"
onClick={() => setEditColumnDescription(record)}
/>
)}
</Space>
);
},
[isReadOnly, hasEditDescriptionPermission]
);
const tableColumn: ColumnsType<Column> = useMemo(
() => [
{
@ -139,7 +106,22 @@ const ModelTab = ({
key: 'description',
accessor: 'description',
width: 350,
render: renderColumnDescription,
render: (_, record, index) => (
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.DASHBOARD_DATA_MODEL}
hasEditPermission={hasEditDescriptionPermission}
index={index}
isReadOnly={isReadOnly}
onClick={() => setEditColumnDescription(record)}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
{
title: t('label.tag-plural'),
@ -149,6 +131,9 @@ const ModelTab = ({
width: 300,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.DASHBOARD_DATA_MODEL}
handleTagSelection={handleFieldTagsChange}
hasTagEditAccess={hasEditTagsPermission}
index={index}
@ -156,6 +141,7 @@ const ModelTab = ({
record={record}
tags={tags}
type={TagSource.Classification}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
@ -167,6 +153,9 @@ const ModelTab = ({
width: 300,
render: (tags: TagLabel[], record: Column, index: number) => (
<TableTags<Column>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.DASHBOARD_DATA_MODEL}
handleTagSelection={handleFieldTagsChange}
hasTagEditAccess={hasEditTagsPermission}
index={index}
@ -174,15 +163,19 @@ const ModelTab = ({
record={record}
tags={tags}
type={TagSource.Glossary}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
],
[
entityFqn,
isReadOnly,
entityFieldThreads,
hasEditTagsPermission,
editColumnDescription,
hasEditDescriptionPermission,
onThreadLinkSelect,
handleFieldTagsChange,
]
);

View File

@ -10,12 +10,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ThreadType } from 'generated/api/feed/createThread';
import { Column } from 'generated/entity/data/dashboardDataModel';
import { EntityFieldThreads } from 'interface/feed.interface';
export interface ModelTabProps {
data: Column[];
entityFqn: string;
isReadOnly: boolean;
hasEditTagsPermission: boolean;
hasEditDescriptionPermission: boolean;
entityFieldThreads: EntityFieldThreads[];
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
onUpdate: (updatedDataModel: Column[]) => Promise<void>;
}

View File

@ -12,11 +12,16 @@
*/
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
import { ThreadType } from 'generated/api/feed/createThread';
import { Mlmodel } from 'generated/entity/data/mlmodel';
import { EntityFieldThreads } from 'interface/feed.interface';
export interface MlModelFeaturesListProp {
mlFeatures: Mlmodel['mlFeatures'];
permissions: OperationPermission;
handleFeaturesUpdate: (features: Mlmodel['mlFeatures']) => Promise<void>;
isDeleted?: boolean;
entityFqn: string;
entityFieldThreads: EntityFieldThreads[];
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}

View File

@ -142,6 +142,7 @@ const settingsUpdateHandler = jest.fn();
const mockProp = {
mlModelDetail: mockData as Mlmodel,
activeTab: 1,
fetchMlModel: jest.fn(),
followMlModelHandler,
unFollowMlModelHandler,
descriptionUpdateHandler,

View File

@ -63,6 +63,7 @@ import MlModelFeaturesList from './MlModelFeaturesList';
const MlModelDetail: FC<MlModelDetailProp> = ({
mlModelDetail,
fetchMlModel,
followMlModelHandler,
unFollowMlModelHandler,
descriptionUpdateHandler,
@ -84,9 +85,6 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
const [mlModelPermissions, setPipelinePermissions] = useState(
DEFAULT_ENTITY_PERMISSION
@ -126,7 +124,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
[AppState.nonSecureUserDetails, AppState.userDetails]
);
const { mlModelTags, isFollowing, tier } = useMemo(() => {
const { mlModelTags, isFollowing, tier, entityFqn } = useMemo(() => {
return {
...mlModelDetail,
tier: getTierTags(mlModelDetail.tags ?? []),
@ -135,6 +133,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
isFollowing: mlModelDetail.followers?.some(
({ id }: { id: string }) => id === currentUser?.id
),
entityFqn: mlModelDetail.fullyQualifiedName ?? '',
};
}, [mlModelDetail]);
@ -143,7 +142,6 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
EntityType.MLMODEL,
mlModelFqn,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
@ -408,10 +406,16 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
onThreadLinkSelect={handleThreadLinkSelect}
/>
<MlModelFeaturesList
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.ML_FEATURES,
entityFieldThreadCount
)}
entityFqn={entityFqn}
handleFeaturesUpdate={onFeaturesUpdate}
isDeleted={mlModelDetail.deleted}
mlFeatures={mlModelDetail.mlFeatures}
permissions={mlModelPermissions}
onThreadLinkSelect={handleThreadLinkSelect}
/>
</div>
</Col>
@ -470,6 +474,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
entityType={EntityType.MLMODEL}
fqn={mlModelDetail?.fullyQualifiedName ?? ''}
onFeedUpdate={fetchEntityFeedCount}
onUpdateEntityDetails={fetchMlModel}
/>
</ActivityFeedProvider>
),
@ -533,7 +538,6 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
mlModelPermissions,
isEdit,
entityFieldThreadCount,
entityFieldTaskCount,
getMlHyperParameters,
getMlModelStore,
onCancel,

View File

@ -18,6 +18,7 @@ import { Mlmodel } from '../../generated/entity/data/mlmodel';
export interface MlModelDetailProp extends HTMLAttributes<HTMLDivElement> {
mlModelDetail: Mlmodel;
version?: string;
fetchMlModel: () => void;
followMlModelHandler: () => Promise<void>;
unFollowMlModelHandler: () => Promise<void>;
descriptionUpdateHandler: (updatedMlModel: Mlmodel) => Promise<void>;

View File

@ -126,6 +126,14 @@ jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
jest.mock('components/TableTags/TableTags.component', () => {
return jest.fn().mockReturnValue(<p>TableTags</p>);
});
jest.mock('components/TableDescription/TableDescription.component', () => {
return jest.fn().mockReturnValue(<p>TableDescription</p>);
});
jest.mock('../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor', () => ({
ModalWithMarkdownEditor: jest
.fn()
@ -152,6 +160,16 @@ const mockProp = {
mlFeatures: mockData['mlFeatures'] as Mlmodel['mlFeatures'],
handleFeaturesUpdate,
permissions: DEFAULT_ENTITY_PERMISSION,
onThreadLinkSelect: jest.fn(),
entityFieldThreads: [
{
entityLink:
'<#E::mlmodel::mlflow_svc.eta_predictions::mlFeatures::sales::description>',
count: 1,
entityField: 'mlFeatures::sales::description',
},
],
entityFqn: 'mlflow_svc.eta_predictions',
};
describe('Test MlModel feature list', () => {

View File

@ -11,9 +11,10 @@
* limitations under the License.
*/
import { Button, Card, Col, Divider, Row, Space, Typography } from 'antd';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { Card, Col, Divider, Row, Space, Typography } from 'antd';
import TableDescription from 'components/TableDescription/TableDescription.component';
import TableTags from 'components/TableTags/TableTags.component';
import { EntityType } from 'enums/entity.enum';
import { TagSource } from 'generated/type/schema';
import { isEmpty } from 'lodash';
import { EntityTags } from 'Models';
@ -22,7 +23,6 @@ import { useTranslation } from 'react-i18next';
import { MlFeature } from '../../generated/entity/data/mlmodel';
import { LabelType, State } from '../../generated/type/tagLabel';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import { MlModelFeaturesListProp } from './MlModel.interface';
import SourceList from './SourceList.component';
@ -32,6 +32,9 @@ const MlModelFeaturesList = ({
handleFeaturesUpdate,
permissions,
isDeleted,
entityFqn,
entityFieldThreads,
onThreadLinkSelect,
}: MlModelFeaturesListProp) => {
const { t } = useTranslation();
const [selectedFeature, setSelectedFeature] = useState<MlFeature>(
@ -153,6 +156,9 @@ const MlModelFeaturesList = ({
<Col flex="auto">
<TableTags<MlFeature>
showInlineEditTagButton
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.MLMODEL}
handleTagSelection={handleTagsChange}
hasTagEditAccess={hasEditPermission}
index={index}
@ -160,6 +166,7 @@ const MlModelFeaturesList = ({
record={feature}
tags={feature.tags ?? []}
type={TagSource.Glossary}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Col>
</Row>
@ -175,6 +182,9 @@ const MlModelFeaturesList = ({
<Col flex="auto">
<TableTags<MlFeature>
showInlineEditTagButton
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.MLMODEL}
handleTagSelection={handleTagsChange}
hasTagEditAccess={hasEditPermission}
index={index}
@ -182,6 +192,7 @@ const MlModelFeaturesList = ({
record={feature}
tags={feature.tags ?? []}
type={TagSource.Classification}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Col>
</Row>
@ -195,31 +206,25 @@ const MlModelFeaturesList = ({
</Typography.Text>
</Col>
<Col flex="auto">
<Space align="start">
{feature.description ? (
<RichTextEditorPreviewer
markdown={feature.description}
/>
) : (
<Typography.Text className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</Typography.Text>
)}
{(permissions.EditAll ||
permissions.EditDescription) && (
<Button
className="m-l-xxs no-border p-0 text-primary h-auto"
icon={<EditIcon width={16} />}
type="text"
onClick={() => {
setSelectedFeature(feature);
setEditDescription(true);
}}
/>
)}
</Space>
<TableDescription
columnData={{
fqn: feature.fullyQualifiedName ?? '',
description: feature.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.MLMODEL}
hasEditPermission={
permissions.EditAll || permissions.EditDescription
}
index={index}
isReadOnly={isDeleted}
onClick={() => {
setSelectedFeature(feature);
setEditDescription(true);
}}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Col>
</Row>
</Col>

View File

@ -11,9 +11,8 @@
* limitations under the License.
*/
import { Card, Col, Radio, Row, Space, Tabs, Tooltip, Typography } from 'antd';
import { Card, Col, Radio, Row, Space, Tabs, Typography } from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { AxiosError } from 'axios';
import ActivityFeedProvider, {
useActivityFeedProvider,
@ -28,6 +27,7 @@ import { DataAssetsHeader } from 'components/DataAssets/DataAssetsHeader/DataAss
import EntityLineageComponent from 'components/EntityLineage/EntityLineage.component';
import ExecutionsTab from 'components/Execution/Execution.component';
import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface';
import TableDescription from 'components/TableDescription/TableDescription.component';
import TableTags from 'components/TableTags/TableTags.component';
import TabsLabel from 'components/TabsLabel/TabsLabel.component';
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
@ -36,7 +36,7 @@ import { EntityField } from 'constants/Feeds.constants';
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
import { compare } from 'fast-json-patch';
import { TagSource } from 'generated/type/schema';
import { isEmpty, isUndefined, map, noop } from 'lodash';
import { isEmpty, isUndefined, map } from 'lodash';
import { EntityTags, TagOption } from 'Models';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -71,16 +71,16 @@ import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
import { PipeLineDetailsProp } from './PipelineDetails.interface';
const PipelineDetails = ({
pipelineDetails,
descriptionUpdateHandler,
followers,
pipelineDetails,
fetchPipeline,
descriptionUpdateHandler,
followPipelineHandler,
unFollowPipelineHandler,
settingsUpdateHandler,
@ -101,6 +101,7 @@ const PipelineDetails = ({
entityName,
tier,
tags,
entityFqn,
} = useMemo(() => {
return {
deleted: pipelineDetails.deleted,
@ -112,6 +113,7 @@ const PipelineDetails = ({
tier: getTierTags(pipelineDetails.tags ?? []),
tags: getTagsWithoutTier(pipelineDetails.tags ?? []),
entityName: getEntityName(pipelineDetails),
entityFqn: pipelineDetails.fullyQualifiedName ?? '',
};
}, [pipelineDetails]);
@ -164,7 +166,6 @@ const PipelineDetails = ({
EntityType.PIPELINE,
pipelineFQN,
setEntityFieldThreadCount,
noop,
setFeedCount
);
};
@ -389,43 +390,26 @@ const PipelineDetails = ({
dataIndex: 'description',
width: 350,
title: t('label.description'),
render: (text, record, index) => (
<Space
className="w-full tw-group cursor-pointer"
data-testid="description">
<div>
{text ? (
<RichTextEditorPreviewer markdown={text} />
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</span>
)}
</div>
{!deleted && (
<Tooltip
title={
pipelinePermissions.EditDescription ||
pipelinePermissions.EditAll
? t('label.edit-entity', { entity: t('label.description') })
: t('message.no-permission-for-action')
}>
<button
className="tw-self-start tw-w-8 tw-h-auto tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none"
disabled={
!(
pipelinePermissions.EditDescription ||
pipelinePermissions.EditAll
)
}
onClick={() => setEditTask({ task: record, index })}>
<EditIcon width={16} />
</button>
</Tooltip>
render: (_, record, index) => (
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
}}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.TASKS,
entityFieldThreadCount
)}
</Space>
entityFqn={entityFqn}
entityType={EntityType.PIPELINE}
hasEditPermission={
pipelinePermissions.EditDescription || pipelinePermissions.EditAll
}
index={index}
isReadOnly={deleted}
onClick={() => setEditTask({ task: record, index })}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
{
@ -436,6 +420,12 @@ const PipelineDetails = ({
width: 300,
render: (tags, record, index) => (
<TableTags<Task>
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.TASKS,
entityFieldThreadCount
)}
entityFqn={entityFqn}
entityType={EntityType.PIPELINE}
handleTagSelection={handleTableTagSelection}
hasTagEditAccess={hasTagEditAccess}
index={index}
@ -443,6 +433,7 @@ const PipelineDetails = ({
record={record}
tags={tags}
type={TagSource.Classification}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
@ -454,6 +445,12 @@ const PipelineDetails = ({
width: 300,
render: (tags, record, index) => (
<TableTags<Task>
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.TASKS,
entityFieldThreadCount
)}
entityFqn={entityFqn}
entityType={EntityType.PIPELINE}
handleTagSelection={handleTableTagSelection}
hasTagEditAccess={hasTagEditAccess}
index={index}
@ -461,6 +458,7 @@ const PipelineDetails = ({
record={record}
tags={tags}
type={TagSource.Glossary}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
@ -468,9 +466,14 @@ const PipelineDetails = ({
[
deleted,
editTask,
entityFqn,
hasTagEditAccess,
pipelinePermissions,
entityFieldThreadCount,
getEntityName,
onThreadLinkSelect,
handleTableTagSelection,
getEntityFieldThreadCounts,
]
);
@ -645,6 +648,7 @@ const PipelineDetails = ({
entityType={EntityType.PIPELINE}
fqn={pipelineDetails?.fullyQualifiedName ?? ''}
onFeedUpdate={getEntityFeedCount}
onUpdateEntityDetails={fetchPipeline}
/>
</ActivityFeedProvider>
),

View File

@ -21,6 +21,7 @@ export interface PipeLineDetailsProp {
pipelineDetails: Pipeline;
followers: Array<EntityReference>;
paging: Paging;
fetchPipeline: () => void;
followPipelineHandler: (fetchCount: () => void) => Promise<void>;
unFollowPipelineHandler: (fetchCount: () => void) => Promise<void>;
settingsUpdateHandler: (updatedPipeline: Pipeline) => Promise<void>;

View File

@ -98,6 +98,7 @@ const PipelineDetailsProps = {
pipelineTags: [],
slashedPipelineName: [],
taskUpdateHandler: mockTaskUpdateHandler,
fetchPipeline: jest.fn(),
setActiveTabHandler: jest.fn(),
followPipelineHandler: jest.fn(),
unFollowPipelineHandler: jest.fn(),

View File

@ -15,7 +15,7 @@ import { t } from 'i18next';
import { lowerCase } from 'lodash';
import React, { Fragment, FunctionComponent, useState } from 'react';
import Searchbar from '../common/searchbar/Searchbar';
import EntityTableV1 from '../EntityTable/EntityTable.component';
import SchemaTable from '../SchemaTable/SchemaTable.component';
import { Props } from './SchemaTab.interfaces';
const SchemaTab: FunctionComponent<Props> = ({
@ -27,11 +27,9 @@ const SchemaTab: FunctionComponent<Props> = ({
hasTagEditAccess,
entityFieldThreads,
onThreadLinkSelect,
onEntityFieldSelect,
isReadOnly = false,
entityFqn,
tableConstraints,
entityFieldTasks,
}: Props) => {
const [searchText, setSearchText] = useState('');
@ -51,9 +49,8 @@ const SchemaTab: FunctionComponent<Props> = ({
/>
</div>
<EntityTableV1
<SchemaTable
columnName={columnName}
entityFieldTasks={entityFieldTasks}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
hasDescriptionEditAccess={hasDescriptionEditAccess}
@ -63,7 +60,6 @@ const SchemaTab: FunctionComponent<Props> = ({
searchText={lowerCase(searchText)}
tableColumns={columns}
tableConstraints={tableConstraints}
onEntityFieldSelect={onEntityFieldSelect}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={onUpdate}
/>

View File

@ -25,13 +25,11 @@ export type Props = {
columnName: string;
tableConstraints: Table['tableConstraints'];
sampleData?: TableData;
hasDescriptionEditAccess?: boolean;
hasDescriptionEditAccess: boolean;
hasTagEditAccess: boolean;
isReadOnly?: boolean;
entityFqn?: string;
entityFieldThreads?: EntityFieldThreads[];
entityFieldTasks?: EntityFieldThreads[];
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
onEntityFieldSelect?: (value: string) => void;
entityFqn: string;
entityFieldThreads: EntityFieldThreads[];
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
onUpdate: (columns: Table['columns']) => Promise<void>;
};

View File

@ -69,8 +69,8 @@ jest.mock('../SampleDataTable/SampleDataTable.component', () => {
return jest.fn().mockReturnValue(<p>SampleDataTable</p>);
});
jest.mock('../EntityTable/EntityTable.component', () => {
return jest.fn().mockReturnValue(<p>EntityTableV1</p>);
jest.mock('../SchemaTable/SchemaTable.component', () => {
return jest.fn().mockReturnValue(<p>SchemaTable</p>);
});
const mockTableConstraints = [
@ -88,9 +88,20 @@ describe('Test SchemaTab Component', () => {
hasTagEditAccess
columnName="columnName"
columns={mockColumns}
entityFieldThreads={[
{
entityLink:
'<#E::table::sample_data.ecommerce_db.shopify.raw_customer::columns::comments::tags>',
count: 4,
entityField: 'columns::comments::tags',
},
]}
entityFqn="mlflow_svc.eta_predictions"
isReadOnly={false}
joins={mockjoins}
sampleData={mockSampleData}
tableConstraints={mockTableConstraints}
onThreadLinkSelect={jest.fn()}
onUpdate={mockUpdate}
/>,
{
@ -101,7 +112,7 @@ describe('Test SchemaTab Component', () => {
expect(searchBar).toBeInTheDocument();
const schemaTable = getByText(container, /EntityTable/i);
const schemaTable = getByText(container, /SchemaTable/i);
expect(schemaTable).toBeInTheDocument();
expect(queryByTestId('sample-data-table')).toBeNull();

View File

@ -11,12 +11,11 @@
* limitations under the License.
*/
import { Button, Popover, Space, Table, Typography } from 'antd';
import { Popover, Space, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg';
import FilterTablePlaceHolder from 'components/common/error-with-placeholder/FilterTablePlaceHolder';
import TableDescription from 'components/TableDescription/TableDescription.component';
import TableTags from 'components/TableTags/TableTags.component';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { TABLE_SCROLL_VALUE } from 'constants/Table.constants';
import { LabelType, State, TagSource } from 'generated/type/schema';
import {
@ -30,42 +29,25 @@ import {
toLower,
} from 'lodash';
import { EntityTags, TagOption } from 'Models';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { ReactComponent as IconRequest } from '../../assets/svg/request-icon.svg';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { EntityField } from '../../constants/Feeds.constants';
import { EntityType, FqnPart } from '../../enums/entity.enum';
import { EntityType } from '../../enums/entity.enum';
import { Column } from '../../generated/entity/data/table';
import { ThreadType } from '../../generated/entity/feed/thread';
import { TagLabel } from '../../generated/type/tagLabel';
import { EntityFieldThreads } from '../../interface/feed.interface';
import { getPartialNameFromTableFQN } from '../../utils/CommonUtils';
import {
ENTITY_LINK_SEPARATOR,
getEntityName,
getFrequentlyJoinedColumns,
} from '../../utils/EntityUtils';
import { getFieldThreadElement } from '../../utils/FeedElementUtils';
import {
getDataTypeString,
getTableExpandableConfig,
makeData,
prepareConstraintIcon,
} from '../../utils/TableUtils';
import {
getRequestDescriptionPath,
getRequestTagsPath,
getUpdateDescriptionPath,
getUpdateTagsPath,
} from '../../utils/TasksUtils';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import { EntityTableProps, TableCellRendered } from './EntityTable.interface';
import './EntityTable.style.less';
import { SchemaTableProps, TableCellRendered } from './SchemaTable.interface';
const EntityTable = ({
const SchemaTable = ({
tableColumns,
searchText,
onUpdate,
@ -77,9 +59,7 @@ const EntityTable = ({
onThreadLinkSelect,
entityFqn,
tableConstraints,
entityFieldTasks,
}: EntityTableProps) => {
const history = useHistory();
}: SchemaTableProps) => {
const { t } = useTranslation();
const [searchedColumns, setSearchedColumns] = useState<Column[]>([]);
@ -233,96 +213,10 @@ const EntityTable = ({
return searchedValue;
};
const getColumnName = (cell: Column) => {
const fqn = cell?.fullyQualifiedName || '';
const columnName = getPartialNameFromTableFQN(fqn, [FqnPart.NestedColumn]);
// wrap it in quotes if dot is present
return columnName.includes(FQN_SEPARATOR_CHAR)
? `"${columnName}"`
: columnName;
};
const onRequestDescriptionHandler = (cell: Column) => {
const field = EntityField.COLUMNS;
const value = getColumnName(cell);
history.push(
getRequestDescriptionPath(
EntityType.TABLE,
entityFqn as string,
field,
value
)
);
};
const onUpdateDescriptionHandler = (cell: Column) => {
const field = EntityField.COLUMNS;
const value = getColumnName(cell);
history.push(
getUpdateDescriptionPath(
EntityType.TABLE,
entityFqn as string,
field,
value
)
);
};
const onRequestTagsHandler = (cell: Column) => {
const field = EntityField.COLUMNS;
const value = getColumnName(cell);
history.push(
getRequestTagsPath(EntityType.TABLE, entityFqn as string, field, value)
);
};
const onUpdateTagsHandler = (cell: Column) => {
const field = EntityField.COLUMNS;
const value = getColumnName(cell);
history.push(
getUpdateTagsPath(EntityType.TABLE, entityFqn as string, field, value)
);
};
const handleUpdate = (column: Column, index: number) => {
handleEditColumn(column, index);
};
const getRequestDescriptionElement = (cell: Column) => {
const hasDescription = Boolean(cell?.description ?? '');
return (
<Button
className="p-0 w-7 h-7 flex-none flex-center link-text focus:tw-outline-none hover-cell-icon m-r-xss"
data-testid="request-description"
type="text"
onClick={() =>
hasDescription
? onUpdateDescriptionHandler(cell)
: onRequestDescriptionHandler(cell)
}>
<Popover
destroyTooltipOnHide
content={
hasDescription
? t('message.request-update-description')
: t('message.request-description')
}
overlayClassName="ant-popover-request-description"
trigger="hover"
zIndex={9999}>
<IconRequest
height={14}
name={t('message.request-description')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Popover>
</Button>
);
};
const renderDataTypeDisplay: TableCellRendered<Column, 'dataTypeDisplay'> = (
dataTypeDisplay,
record
@ -355,92 +249,35 @@ const EntityTable = ({
};
const renderDescription: TableCellRendered<Column, 'description'> = (
description,
_,
record,
index
) => {
return (
<div className="hover-icon-group">
<div className="d-inline-block">
<Space
data-testid="description"
direction={isEmpty(description) ? 'horizontal' : 'vertical'}
id={`column-description-${index}`}
size={4}>
<div>
{description ? (
<RichTextEditorPreviewer markdown={description} />
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</span>
)}
</div>
<div className="d-flex tw--mt-1.5">
{!isReadOnly ? (
<Fragment>
{hasDescriptionEditAccess && (
<>
<Button
className="p-0 tw-self-start flex-center w-7 h-7 d-flex-none hover-cell-icon"
type="text"
onClick={() => handleUpdate(record, index)}>
<IconEdit
height={14}
name={t('label.edit')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Button>
</>
)}
{getRequestDescriptionElement(record)}
{getFieldThreadElement(
getColumnName(record),
EntityField.DESCRIPTION,
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
`columns${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}description`,
Boolean(record)
)}
{getFieldThreadElement(
getColumnName(record),
EntityField.DESCRIPTION,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
`columns${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}description`,
Boolean(record),
ThreadType.Task
)}
</Fragment>
) : null}
</div>
</Space>
</div>
<>
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.TABLE}
hasEditPermission={hasDescriptionEditAccess}
index={index}
isReadOnly={isReadOnly}
onClick={() => handleUpdate(record, index)}
onThreadLinkSelect={onThreadLinkSelect}
/>
{getFrequentlyJoinedColumns(
record?.name,
joins,
t('label.frequently-joined-column-plural')
)}
</div>
</>
);
};
const getColumnFieldFQN = (record: Column) =>
`${EntityField.COLUMNS}${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}${EntityField.TAGS}`;
const columns: ColumnsType<Column> = useMemo(
() => [
{
@ -487,8 +324,7 @@ const EntityTable = ({
<TableTags<Column>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
getColumnFieldFQN={getColumnFieldFQN(record)}
getColumnName={getColumnName}
entityType={EntityType.TABLE}
handleTagSelection={handleTagSelection}
hasTagEditAccess={hasTagEditAccess}
index={index}
@ -496,9 +332,7 @@ const EntityTable = ({
record={record}
tags={tags}
type={TagSource.Classification}
onRequestTagsHandler={onRequestTagsHandler}
onThreadLinkSelect={onThreadLinkSelect}
onUpdateTagsHandler={onUpdateTagsHandler}
/>
),
},
@ -512,8 +346,7 @@ const EntityTable = ({
<TableTags<Column>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
getColumnFieldFQN={getColumnFieldFQN(record)}
getColumnName={getColumnName}
entityType={EntityType.TABLE}
handleTagSelection={handleTagSelection}
hasTagEditAccess={hasTagEditAccess}
index={index}
@ -521,9 +354,7 @@ const EntityTable = ({
record={record}
tags={tags}
type={TagSource.Glossary}
onRequestTagsHandler={onRequestTagsHandler}
onThreadLinkSelect={onThreadLinkSelect}
onUpdateTagsHandler={onUpdateTagsHandler}
/>
),
},
@ -531,7 +362,6 @@ const EntityTable = ({
[
entityFqn,
isReadOnly,
entityFieldTasks,
entityFieldThreads,
tableConstraints,
hasTagEditAccess,
@ -539,10 +369,7 @@ const EntityTable = ({
handleTagSelection,
renderDataTypeDisplay,
renderDescription,
getColumnName,
handleTagSelection,
onRequestTagsHandler,
onUpdateTagsHandler,
onThreadLinkSelect,
]
);
@ -592,4 +419,4 @@ const EntityTable = ({
);
};
export default EntityTable;
export default SchemaTable;

View File

@ -16,21 +16,19 @@ import { ThreadType } from '../../generated/api/feed/createThread';
import { Column, ColumnJoins, Table } from '../../generated/entity/data/table';
import { EntityFieldThreads } from '../../interface/feed.interface';
export interface EntityTableProps {
export interface SchemaTableProps {
tableColumns: Column[];
joins: Array<ColumnJoins>;
columnName: string;
hasDescriptionEditAccess?: boolean;
hasDescriptionEditAccess: boolean;
hasTagEditAccess: boolean;
tableConstraints: Table['tableConstraints'];
searchText?: string;
isReadOnly?: boolean;
entityFqn?: string;
entityFieldThreads?: EntityFieldThreads[];
entityFieldTasks?: EntityFieldThreads[];
entityFqn: string;
entityFieldThreads: EntityFieldThreads[];
onUpdate: (columns: Column[]) => Promise<void>;
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
onEntityFieldSelect?: (value: string) => void;
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}
export type TableCellRendered<T, K extends keyof T> = (

View File

@ -11,13 +11,13 @@
* limitations under the License.
*/
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { Column } from 'generated/entity/data/container';
import { TagOption } from 'Models';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Column } from '../../generated/api/data/createTable';
import { Table } from '../../generated/entity/data/table';
import EntityTableV1 from './EntityTable.component';
import EntityTableV1 from './SchemaTable.component';
const onEntityFieldSelect = jest.fn();
const onThreadLinkSelect = jest.fn();
@ -67,46 +67,12 @@ const mockEntityTableProp = {
constraint: 'NULL',
ordinalPosition: 3,
},
{
name: 'store_address',
dataType: 'ARRAY',
arrayDataType: 'STRUCT',
dataLength: 1,
dataTypeDisplay:
'array<struct<name:character varying(32),street_address:character varying(128),city:character varying(32),postcode:character varying(8)>>',
fullyQualifiedName:
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.store_address',
tags: [],
constraint: 'NULL',
ordinalPosition: 4,
},
{
name: 'first_order_date',
dataType: 'TIMESTAMP',
dataTypeDisplay: 'timestamp',
description:
'The date (ISO 8601) and time (UTC) when the customer placed their first order. The format is YYYY-MM-DD HH:mm:ss (for example, 2016-02-05 17:04:01).',
fullyQualifiedName:
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.first_order_date',
tags: [],
ordinalPosition: 5,
},
{
name: 'last_order_date',
dataType: 'TIMESTAMP',
dataTypeDisplay: 'timestamp',
description:
'The date (ISO 8601) and time (UTC) when the customer placed their most recent order. The format is YYYY-MM-DD HH:mm:ss (for example, 2016-02-05 17:04:01).',
fullyQualifiedName:
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.last_order_date',
tags: [],
ordinalPosition: 6,
},
] as Column[],
searchText: '',
hasEditAccess: false,
joins: [],
entityFieldThreads: [],
hasDescriptionEditAccess: true,
isReadOnly: false,
entityFqn: 'bigquery_gcp.ecommerce.shopify.raw_product_catalog',
owner: {} as Table['owner'],
@ -135,6 +101,13 @@ jest.mock('../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor', () => ({
ModalWithMarkdownEditor: jest.fn().mockReturnValue(<p>EditorModal</p>),
}));
jest.mock(
'components/common/error-with-placeholder/FilterTablePlaceHolder',
() => {
return jest.fn().mockReturnValue(<p>FilterTablePlaceHolder</p>);
}
);
jest.mock('components/Tag/TagsContainer/tags-container', () => {
return jest.fn().mockImplementation(({ tagList }) => {
return (
@ -165,17 +138,22 @@ jest.mock('../../utils/GlossaryUtils', () => ({
getGlossaryTermHierarchy: jest.fn().mockReturnValue([]),
}));
jest.mock(
'components/common/error-with-placeholder/FilterTablePlaceHolder',
() => {
return jest.fn().mockReturnValue(<p>FilterTablePlaceHolder</p>);
}
);
jest.mock('components/TableTags/TableTags.component', () => {
return jest.fn().mockReturnValue(<p>TableTags</p>);
});
jest.mock('components/TableDescription/TableDescription.component', () => {
return jest.fn().mockReturnValue(<p>TableDescription</p>);
});
const mockTableScrollValue = jest.fn();
jest.mock('constants/Table.constants', () => ({
get TABLE_SCROLL_VALUE() {
return mockTableScrollValue();
},
}));
describe('Test EntityTable Component', () => {
it('Initially, Table should load', async () => {
render(<EntityTableV1 {...mockEntityTableProp} />, {
@ -184,45 +162,36 @@ describe('Test EntityTable Component', () => {
const entityTable = await screen.findByTestId('entity-table');
screen.debug(entityTable);
expect(entityTable).toBeInTheDocument();
});
it('should render request description button', async () => {
it('Should render tags and description components', async () => {
render(<EntityTableV1 {...mockEntityTableProp} />, {
wrapper: MemoryRouter,
});
const tableTags = screen.getAllByText('TableTags');
expect(tableTags).toHaveLength(6);
const tableDescription = screen.getAllByText('TableDescription');
expect(tableDescription).toHaveLength(3);
});
it('Table should load empty when no data present', async () => {
render(<EntityTableV1 {...mockEntityTableProp} tableColumns={[]} />, {
wrapper: MemoryRouter,
});
const entityTable = await screen.findByTestId('entity-table');
expect(entityTable).toBeInTheDocument();
const requestDescriptionButton = await screen.findAllByTestId(
'request-description'
);
const emptyPlaceholder = screen.getByText('FilterTablePlaceHolder');
expect(requestDescriptionButton[0]).toBeInTheDocument();
});
it('Should render start thread button', async () => {
render(<EntityTableV1 {...mockEntityTableProp} />, {
wrapper: MemoryRouter,
});
const entityTable = await screen.findByTestId('entity-table');
expect(entityTable).toBeInTheDocument();
const startThreadButton = await screen.findAllByTestId(
'start-field-thread'
);
expect(startThreadButton[0]).toBeInTheDocument();
fireEvent.click(
startThreadButton[0],
new MouseEvent('click', { bubbles: true, cancelable: true })
);
expect(onThreadLinkSelect).toHaveBeenCalled();
expect(emptyPlaceholder).toBeInTheDocument();
});
});

View File

@ -0,0 +1,78 @@
/*
* 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.
*/
import { Space } from 'antd';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import EntityTaskDescription from 'pages/TasksPage/EntityTaskDescription/EntityTaskDescription.component';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../assets/svg/edit-new.svg';
import { TableDescriptionProps } from './TableDescription.interface';
const TableDescription = ({
index,
columnData,
entityFqn,
isReadOnly,
onClick,
entityType,
hasEditPermission,
entityFieldThreads,
onThreadLinkSelect,
}: TableDescriptionProps) => {
const { t } = useTranslation();
return (
<Space
className="hover-icon-group"
data-testid="description"
direction="vertical"
id={`field-description-${index}`}>
{columnData.description ? (
<RichTextEditorPreviewer markdown={columnData.description} />
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</span>
)}
{!isReadOnly ? (
<Space align="baseline" size="middle">
{hasEditPermission && (
<EditIcon
className="cursor-pointer hover-cell-icon"
data-testid="edit-button"
height={14}
name={t('label.edit')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
onClick={onClick}
/>
)}
<EntityTaskDescription
data={columnData}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={entityType}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Space>
) : null}
</Space>
);
};
export default TableDescription;

View File

@ -0,0 +1,31 @@
/*
* 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.
*/
import { EntityType } from 'enums/entity.enum';
import { ThreadType } from 'generated/entity/feed/thread';
import { EntityFieldThreads } from 'interface/feed.interface';
export interface TableDescriptionProps {
index: number;
columnData: {
fqn: string;
description?: string;
};
entityFqn: string;
entityType: EntityType;
hasEditPermission: boolean;
entityFieldThreads: EntityFieldThreads[];
isReadOnly?: boolean;
onClick: () => void;
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}

View File

@ -11,19 +11,10 @@
* limitations under the License.
*/
import { Button, Tooltip } from 'antd';
import classNames from 'classnames';
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { EntityField } from 'constants/Feeds.constants';
import { EntityType } from 'enums/entity.enum';
import { TagSource } from 'generated/type/tagLabel';
import { EntityFieldThreads } from 'interface/feed.interface';
import { isEmpty } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getFieldThreadElement } from 'utils/FeedElementUtils';
import { ReactComponent as IconRequest } from '../../assets/svg/request-icon.svg';
import EntityTaskTags from 'pages/TasksPage/EntityTaskTags/EntityTaskTags.component';
import React from 'react';
import { TableTagsComponentProps, TableUnion } from './TableTags.interface';
const TableTags = <T extends TableUnion>({
@ -35,64 +26,11 @@ const TableTags = <T extends TableUnion>({
isReadOnly,
hasTagEditAccess,
entityFieldThreads,
getColumnFieldFQN,
showInlineEditTagButton,
getColumnName,
onUpdateTagsHandler,
onRequestTagsHandler,
onThreadLinkSelect,
handleTagSelection,
entityType,
}: TableTagsComponentProps<T>) => {
const { t } = useTranslation();
const hasTagOperationAccess = useMemo(
() =>
getColumnFieldFQN &&
getColumnName &&
onUpdateTagsHandler &&
onRequestTagsHandler,
[
getColumnFieldFQN,
getColumnName,
onUpdateTagsHandler,
onRequestTagsHandler,
]
);
const getRequestTagsElement = useMemo(() => {
const hasTags = !isEmpty(record.tags || []);
return (
<Tooltip
destroyTooltipOnHide
overlayClassName="ant-popover-request-description"
title={
hasTags
? t('label.update-request-tag-plural')
: t('label.request-tag-plural')
}>
<Button
className="p-0 w-7 h-7 flex-center m-r-xss link-text hover-cell-icon"
data-testid="request-tags"
icon={
<IconRequest
height={14}
name={t('label.request-tag-plural')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
}
type="text"
onClick={() =>
hasTags
? onUpdateTagsHandler?.(record)
: onRequestTagsHandler?.(record)
}
/>
</Tooltip>
);
}, [record, onUpdateTagsHandler, onRequestTagsHandler]);
return (
<div className="hover-icon-group" data-testid={`${type}-tags-${index}`}>
<div
@ -110,26 +48,17 @@ const TableTags = <T extends TableUnion>({
}}>
<>
{!isReadOnly && (
<div className="d-flex items-center">
{hasTagOperationAccess && (
<>
{/* Request and Update tags */}
{type === TagSource.Classification && getRequestTagsElement}
{/* List Conversation */}
{getFieldThreadElement(
getColumnName?.(record) ?? '',
EntityField.TAGS,
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TABLE,
entityFqn,
getColumnFieldFQN,
Boolean(record?.name?.length)
)}
</>
)}
</div>
<EntityTaskTags
data={{
fqn: record.fullyQualifiedName ?? '',
tags: record.tags ?? [],
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={entityType}
tagSource={type}
onThreadLinkSelect={onThreadLinkSelect}
/>
)}
</>
</TagsContainerV2>

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { EntityType } from 'enums/entity.enum';
import { MlFeature } from 'generated/entity/data/mlmodel';
import { Task } from 'generated/entity/data/pipeline';
import { Field } from 'generated/entity/data/topic';
@ -23,23 +24,20 @@ import { EntityFieldThreads } from '../../interface/feed.interface';
export interface TableTagsComponentProps<T> {
tags: TagLabel[];
onUpdateTagsHandler?: (cell: T) => void;
isReadOnly?: boolean;
entityFqn?: string;
entityFqn: string;
record: T;
index: number;
hasTagEditAccess: boolean;
entityFieldThreads: EntityFieldThreads[];
type: TagSource;
showInlineEditTagButton?: boolean;
entityType: EntityType;
handleTagSelection: (
selectedTags: EntityTags[],
editColumnTag: T
) => Promise<void>;
onRequestTagsHandler?: (cell: T) => void;
getColumnName?: (cell: T) => string;
getColumnFieldFQN?: string;
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
entityFieldThreads?: EntityFieldThreads[];
type: TagSource;
showInlineEditTagButton?: boolean;
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}
export interface TagsCollection {

View File

@ -12,6 +12,7 @@
*/
import { render, screen } from '@testing-library/react';
import { EntityType } from 'enums/entity.enum';
import { Constraint, DataType } from 'generated/entity/data/table';
import { LabelType, State, TagSource } from 'generated/type/schema';
import React from 'react';
@ -26,6 +27,10 @@ jest.mock('utils/FeedElementUtils', () => ({
),
}));
jest.mock('pages/TasksPage/EntityTaskTags/EntityTaskTags.component', () => {
return jest.fn().mockImplementation(() => <div>EntityTaskTags</div>);
});
const glossaryTags = [
{
tagFQN: 'glossary.term1',
@ -92,6 +97,7 @@ const mockProp = {
entityFqn: 'sample_data.ecommerce_db.shopify.raw_customer',
handleTagSelection: jest.fn(),
type: TagSource.Classification,
entityType: EntityType.TABLE,
};
describe('Test EntityTableTags Component', () => {
@ -148,10 +154,11 @@ describe('Test EntityTableTags Component', () => {
expect(tagPersonal).toBeInTheDocument();
});
it('Should not render update and request tags buttons', async () => {
it('Should not render entity task component if entity is deleted', async () => {
render(
<TableTags
{...mockProp}
isReadOnly
record={{
...mockProp.record,
tags: [...classificationTags, ...glossaryTags],
@ -164,10 +171,10 @@ describe('Test EntityTableTags Component', () => {
);
const tagContainer = await screen.findByTestId('Classification-tags-0');
const requestTags = screen.queryByTestId('field-thread-element');
const entityTaskTags = screen.queryByText('EntityTaskTags');
expect(tagContainer).toBeInTheDocument();
expect(requestTags).not.toBeInTheDocument();
expect(entityTaskTags).not.toBeInTheDocument();
});
it('Should render update and request tags buttons', async () => {
@ -187,9 +194,9 @@ describe('Test EntityTableTags Component', () => {
);
const tagContainer = await screen.findByTestId('Classification-tags-0');
const requestTags = await screen.findAllByTestId('field-thread-element');
const entityTaskTags = screen.queryByText('EntityTaskTags');
expect(tagContainer).toBeInTheDocument();
expect(requestTags).toHaveLength(1);
expect(entityTaskTags).toBeInTheDocument();
});
});

View File

@ -18,7 +18,7 @@ import { ReactElement } from 'react';
export type TagsContainerV2Props = {
permission: boolean;
isVersionView?: boolean;
showTaskHandler?: boolean;
selectedTags: EntityTags[];
entityType?: string;
entityThreadLink?: string;

View File

@ -11,12 +11,11 @@
* limitations under the License.
*/
import { Button, Col, Form, Row, Space, Tooltip, Typography } from 'antd';
import { Col, Form, Row, Space, Tooltip, Typography } from 'antd';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { TableTagsProps } from 'components/TableTags/TableTags.interface';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
import { EntityType } from 'enums/entity.enum';
import { SearchIndex } from 'enums/search.enum';
import { Paging } from 'generated/type/paging';
import { TagSource } from 'generated/type/tagLabel';
@ -30,11 +29,7 @@ import { formatSearchGlossaryTermResponse } from 'utils/APIUtils';
import { getEntityFeedLink } from 'utils/EntityUtils';
import { getFilterTags } from 'utils/TableTags/TableTags.utils';
import { fetchTagsElasticSearch, getTagPlaceholder } from 'utils/TagsUtils';
import {
getRequestTagsPath,
getUpdateTagsPath,
TASK_ENTITIES,
} from 'utils/TasksUtils';
import { getRequestTagsPath, getUpdateTagsPath } from 'utils/TasksUtils';
import { ReactComponent as IconComments } from '../../../assets/svg/comment.svg';
import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg';
import TagSelectForm from '../TagsSelectForm/TagsSelectForm.component';
@ -44,7 +39,7 @@ import { TagsContainerV2Props } from './TagsContainerV2.interface';
const TagsContainerV2 = ({
permission,
isVersionView,
showTaskHandler = true,
selectedTags,
entityType,
entityThreadLink,
@ -197,71 +192,62 @@ const TagsContainerV2 = ({
handleSave,
]);
const handleRequestTags = () => {
history.push(getRequestTagsPath(entityType as string, entityFqn as string));
};
const handleUpdateTags = () => {
history.push(getUpdateTagsPath(entityType as string, entityFqn as string));
const handleTagsTask = (hasTags: boolean) => {
history.push(
(hasTags ? getUpdateTagsPath : getRequestTagsPath)(
entityType as string,
entityFqn as string
)
);
};
const requestTagElement = useMemo(() => {
const hasTags = !isEmpty(tags?.[tagType]);
return TASK_ENTITIES.includes(entityType as EntityType) ? (
return (
<Col>
<Button
className="p-0 flex-center"
data-testid="request-entity-tags"
size="small"
type="text"
onClick={hasTags ? handleUpdateTags : handleRequestTags}>
<Tooltip
placement="left"
title={
hasTags
? t('label.update-request-tag-plural')
: t('label.request-tag-plural')
}>
<IconRequest
className="anticon"
height={14}
name="request-tags"
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Tooltip>
</Button>
<Tooltip
title={
hasTags
? t('label.update-request-tag-plural')
: t('label.request-tag-plural')
}>
<IconRequest
className="cursor-pointer"
data-testid="request-entity-tags"
height={14}
name="request-tags"
style={{ color: DE_ACTIVE_COLOR }}
width={14}
onClick={() => handleTagsTask(hasTags)}
/>
</Tooltip>
</Col>
) : null;
}, [tags?.[tagType], handleUpdateTags, handleRequestTags]);
);
}, [tags?.[tagType], handleTagsTask]);
const conversationThreadElement = useMemo(
() => (
<Col>
<Button
className="p-0 flex-center"
data-testid="tag-thread"
size="small"
type="text"
onClick={() =>
onThreadLinkSelect?.(
entityThreadLink ??
getEntityFeedLink(entityType, entityFqn, 'tags')
)
}>
<Tooltip
placement="left"
title={t('label.list-entity', {
entity: t('label.conversation'),
})}>
<IconComments
height={14}
name="comments"
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Tooltip>
</Button>
<Tooltip
title={t('label.list-entity', {
entity: t('label.conversation'),
})}>
<IconComments
className="cursor-pointer"
data-testid="tag-thread"
height={14}
name="comments"
style={{ color: DE_ACTIVE_COLOR }}
width={14}
onClick={() =>
onThreadLinkSelect?.(
entityThreadLink ??
getEntityFeedLink(entityType, entityFqn, 'tags')
)
}
/>
</Tooltip>
</Col>
),
[
@ -281,22 +267,23 @@ const TagsContainerV2 = ({
{isGlossaryType ? t('label.glossary-term') : t('label.tag-plural')}
</Typography.Text>
{permission && (
<Row gutter={8}>
<Row gutter={12}>
{!isEmpty(tags?.[tagType]) && !isEditTags && (
<Button
className="cursor-pointer flex-center"
data-testid="edit-button"
icon={<EditIcon color={DE_ACTIVE_COLOR} width="14px" />}
size="small"
type="text"
onClick={handleAddClick}
/>
<Col>
<EditIcon
className="cursor-pointer"
color={DE_ACTIVE_COLOR}
data-testid="edit-button"
width="14px"
onClick={handleAddClick}
/>
</Col>
)}
{permission && !isVersionView && (
<Row gutter={8}>
{showTaskHandler && (
<>
{tagType === TagSource.Classification && requestTagElement}
{onThreadLinkSelect && conversationThreadElement}
</Row>
</>
)}
</Row>
)}
@ -309,7 +296,7 @@ const TagsContainerV2 = ({
showHeader,
isEditTags,
permission,
isVersionView,
showTaskHandler,
isGlossaryType,
requestTagElement,
conversationThreadElement,
@ -318,19 +305,13 @@ const TagsContainerV2 = ({
const editTagButton = useMemo(
() =>
permission && !isEmpty(tags?.[tagType]) ? (
<Button
className="p-0 w-7 h-7 flex-center text-primary hover-cell-icon"
<EditIcon
className="hover-cell-icon cursor-pointer"
data-testid="edit-button"
icon={
<EditIcon
height={14}
name={t('label.edit')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
}
size="small"
type="text"
height={14}
name={t('label.edit')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
onClick={handleAddClick}
/>
) : null,
@ -348,7 +329,7 @@ const TagsContainerV2 = ({
{header}
{!isEditTags && (
<Space wrap align="center" data-testid="entity-tags" size={4}>
<Space wrap data-testid="entity-tags" size={4}>
{addTagButton}
{renderTags}
{showInlineEditButton && editTagButton}
@ -356,10 +337,10 @@ const TagsContainerV2 = ({
)}
{isEditTags && tagsSelectContainer}
<div className="m-t-xss d-flex items-center">
<Space align="baseline" className="m-t-xs w-full" size="middle">
{showBottomEditButton && !showInlineEditButton && editTagButton}
{children}
</div>
</Space>
</div>
);
};

View File

@ -74,6 +74,7 @@ const TagsInput: React.FC<Props> = ({
<TagsContainerV2
permission={editable}
selectedTags={getSelectedTags()}
showTaskHandler={false}
tagType={TagSource.Classification}
onSelectionChange={handleTagSelection}
/>

View File

@ -58,6 +58,7 @@ import TopicSchemaFields from './TopicSchema/TopicSchema';
const TopicDetails: React.FC<TopicDetailsProps> = ({
topicDetails,
fetchTopic,
followTopicHandler,
unFollowTopicHandler,
versionHandler,
@ -76,9 +77,6 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
const [threadType, setThreadType] = useState<ThreadType>(
ThreadType.Conversation
@ -253,7 +251,6 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
EntityType.TOPIC,
topicFQN,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
@ -294,12 +291,8 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
onThreadLinkSelect={onThreadLinkSelect}
/>
<TopicSchemaFields
entityFieldTasks={getEntityFieldThreadCounts(
EntityField.COLUMNS,
entityFieldTaskCount
)}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.COLUMNS,
EntityField.MESSAGE_SCHEMA,
entityFieldThreadCount
)}
entityFqn={topicDetails.fullyQualifiedName ?? ''}
@ -369,6 +362,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
entityType={EntityType.TOPIC}
fqn={topicDetails?.fullyQualifiedName ?? ''}
onFeedUpdate={getEntityFeedCount}
onUpdateEntityDetails={fetchTopic}
/>
</ActivityFeedProvider>
),
@ -443,7 +437,6 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
activeTab,
feedCount,
topicDetails,
entityFieldTaskCount,
entityFieldThreadCount,
topicPermissions,
isEdit,

View File

@ -19,6 +19,7 @@ import { SchemaType } from '../../generated/type/schema';
export interface TopicDetailsProps {
topicDetails: Topic;
topicPermissions: OperationPermission;
fetchTopic: () => void;
createThread: (data: CreateThread) => void;
followTopicHandler: () => Promise<void>;
unFollowTopicHandler: () => Promise<void>;

View File

@ -51,6 +51,7 @@ const mockUserTeam = [
const topicDetailsProps: TopicDetailsProps = {
topicDetails: TOPIC_DETAILS,
fetchTopic: jest.fn(),
followTopicHandler: jest.fn(),
unFollowTopicHandler: jest.fn(),
onTopicUpdate: jest.fn(),

View File

@ -32,10 +32,9 @@ export interface TopicSchemaFieldsProps
entityFqn: string;
defaultExpandAllRows?: boolean;
showSchemaDisplayTypeSwitch?: boolean;
entityFieldThreads: EntityFieldThreads[];
onUpdate?: (updatedMessageSchema: Topic['messageSchema']) => Promise<void>;
entityFieldThreads?: EntityFieldThreads[];
entityFieldTasks?: EntityFieldThreads[];
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}
export enum SchemaViewType {

View File

@ -36,6 +36,15 @@ const mockProps: TopicSchemaFieldsProps = {
onUpdate: mockOnUpdate,
hasTagEditAccess: true,
entityFqn: 'topic.fqn',
entityFieldThreads: [
{
entityLink:
'#E::topic::sample_kafka.address_book::messageSchema::schemaFields::AddressBook::description>',
count: 1,
entityField: 'messageSchema::schemaFields::AddressBook::description',
},
],
onThreadLinkSelect: jest.fn(),
};
jest.mock('utils/TagsUtils', () => ({

View File

@ -24,45 +24,31 @@ import {
} from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import { Key } from 'antd/lib/table/interface';
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { ReactComponent as DownUpArrowIcon } from 'assets/svg/ic-down-up-arrow.svg';
import { ReactComponent as UpDownArrowIcon } from 'assets/svg/ic-up-down-arrow.svg';
import classNames from 'classnames';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
import SchemaEditor from 'components/schema-editor/SchemaEditor';
import TableDescription from 'components/TableDescription/TableDescription.component';
import TableTags from 'components/TableTags/TableTags.component';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { EntityField } from 'constants/Feeds.constants';
import { TABLE_SCROLL_VALUE } from 'constants/Table.constants';
import { CSMode } from 'enums/codemirror.enum';
import { EntityType } from 'enums/entity.enum';
import { ThreadType } from 'generated/api/feed/createThread';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
import { EntityFieldThreads } from 'interface/feed.interface';
import { cloneDeep, isEmpty, isUndefined, map } from 'lodash';
import { EntityTags, TagOption } from 'Models';
import React, { FC, Fragment, useMemo, useState } from 'react';
import React, { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { getPartialNameFromTopicFQN } from 'utils/CommonUtils';
import { ENTITY_LINK_SEPARATOR, getEntityName } from 'utils/EntityUtils';
import { getFieldThreadElement } from 'utils/FeedElementUtils';
import {
getRequestDescriptionPath,
getUpdateDescriptionPath,
} from 'utils/TasksUtils';
import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg';
import { getEntityName } from 'utils/EntityUtils';
import { DataTypeTopic, Field } from '../../../generated/entity/data/topic';
import { getTableExpandableConfig } from '../../../utils/TableUtils';
import {
updateFieldDescription,
updateFieldTags,
} from '../../../utils/TopicSchema.utils';
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import {
CellRendered,
SchemaViewType,
TopicSchemaFieldsProps,
} from './TopicSchema.interface';
@ -79,9 +65,7 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
entityFqn,
entityFieldThreads,
onThreadLinkSelect,
entityFieldTasks,
}) => {
const history = useHistory();
const { t } = useTranslation();
const [editFieldDescription, setEditFieldDescription] = useState<Field>();
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
@ -105,16 +89,6 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
return getAllRowKeys(messageSchema?.schemaFields ?? []);
}, [messageSchema?.schemaFields]);
const getColumnName = (cell: Field) => {
const fqn = cell?.fullyQualifiedName || '';
const columnName = getPartialNameFromTopicFQN(fqn);
// wrap it in quotes if dot is present
return columnName.includes(FQN_SEPARATOR_CHAR)
? `"${columnName}"`
: columnName;
};
const handleFieldTagsChange = async (
selectedTags: EntityTags[],
editColumnTag: Field
@ -162,141 +136,6 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
setExpandedRowKeys(keys as string[]);
};
const onUpdateDescriptionHandler = (cell: Field) => {
const field = EntityField.COLUMNS;
const value = getColumnName(cell);
history.push(
getUpdateDescriptionPath(
EntityType.TOPIC,
entityFqn as string,
field,
value
)
);
};
const onRequestDescriptionHandler = (cell: Field) => {
const field = EntityField.COLUMNS;
const value = getColumnName(cell);
history.push(
getRequestDescriptionPath(
EntityType.TOPIC,
entityFqn as string,
field,
value
)
);
};
const getRequestDescriptionElement = (cell: Field) => {
const hasDescription = Boolean(cell?.description ?? '');
return (
<Button
className="p-0 w-7 h-7 flex-none flex-center link-text focus:tw-outline-none hover-cell-icon m-r-xss"
data-testid="request-description"
type="text"
onClick={() =>
hasDescription
? onUpdateDescriptionHandler(cell)
: onRequestDescriptionHandler(cell)
}>
<Popover
destroyTooltipOnHide
content={
hasDescription
? t('message.request-update-description')
: t('message.request-description')
}
overlayClassName="ant-popover-request-description"
trigger="hover"
zIndex={9999}>
<IconRequest
height={14}
name={t('message.request-description')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Popover>
</Button>
);
};
const renderFieldDescription: CellRendered<Field, 'description'> = (
description,
record,
index
) => {
return (
<Space
className="custom-group w-full"
data-testid="description"
direction={isEmpty(description) ? 'horizontal' : 'vertical'}
id={`field-description-${index}`}
size={4}>
<div>
{description ? (
<RichTextEditorPreviewer markdown={description} />
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</span>
)}
</div>
<div className="d-flex tw--mt-1.5">
{!isReadOnly ? (
<Fragment>
{hasDescriptionEditAccess && (
<>
<Button
className="p-0 tw-self-start flex-center w-7 h-7 d-flex-none hover-cell-icon"
data-testid="edit-button"
type="text"
onClick={() => setEditFieldDescription(record)}>
<EditIcon
height={14}
name={t('label.edit')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
/>
</Button>
</>
)}
{getRequestDescriptionElement(record)}
{getFieldThreadElement(
getColumnName(record),
EntityField.DESCRIPTION,
entityFieldThreads as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TOPIC,
entityFqn,
`columns${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}description`,
Boolean(record)
)}
{getFieldThreadElement(
getColumnName(record),
EntityField.DESCRIPTION,
entityFieldTasks as EntityFieldThreads[],
onThreadLinkSelect,
EntityType.TOPIC,
entityFqn,
`columns${ENTITY_LINK_SEPARATOR}${getColumnName(
record
)}${ENTITY_LINK_SEPARATOR}description`,
Boolean(record),
ThreadType.Task
)}
</Fragment>
) : null}
</div>
</Space>
);
};
const columns: ColumnsType<Field> = useMemo(
() => [
{
@ -339,7 +178,22 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
dataIndex: 'description',
key: 'description',
width: 350,
render: renderFieldDescription,
render: (_, record, index) => (
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.TOPIC}
hasEditPermission={hasDescriptionEditAccess}
index={index}
isReadOnly={isReadOnly}
onClick={() => setEditFieldDescription(record)}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
{
title: t('label.tag-plural'),
@ -349,6 +203,9 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
width: 300,
render: (tags: TagLabel[], record: Field, index: number) => (
<TableTags<Field>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.TOPIC}
handleTagSelection={handleFieldTagsChange}
hasTagEditAccess={hasTagEditAccess}
index={index}
@ -356,6 +213,7 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
record={record}
tags={tags}
type={TagSource.Classification}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
@ -367,6 +225,9 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
width: 300,
render: (tags: TagLabel[], record: Field, index: number) => (
<TableTags<Field>
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityType={EntityType.TOPIC}
handleTagSelection={handleFieldTagsChange}
hasTagEditAccess={hasTagEditAccess}
index={index}
@ -374,6 +235,7 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
record={record}
tags={tags}
type={TagSource.Glossary}
onThreadLinkSelect={onThreadLinkSelect}
/>
),
},
@ -453,26 +315,24 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
/>
)
) : (
<>
<Table
bordered
className={className}
columns={columns}
data-testid="topic-schema-fields-table"
dataSource={messageSchema?.schemaFields}
expandable={{
...getTableExpandableConfig<Field>(),
rowExpandable: (record) => !isEmpty(record.children),
onExpandedRowsChange: handleExpandedRowsChange,
defaultExpandAllRows,
expandedRowKeys,
}}
pagination={false}
rowKey="name"
scroll={TABLE_SCROLL_VALUE}
size="small"
/>
</>
<Table
bordered
className={className}
columns={columns}
data-testid="topic-schema-fields-table"
dataSource={messageSchema?.schemaFields}
expandable={{
...getTableExpandableConfig<Field>(),
rowExpandable: (record) => !isEmpty(record.children),
onExpandedRowsChange: handleExpandedRowsChange,
defaultExpandAllRows,
expandedRowKeys,
}}
pagination={false}
rowKey="name"
scroll={TABLE_SCROLL_VALUE}
size="small"
/>
)}
</Col>
</>

View File

@ -26,6 +26,7 @@ import { EntityField } from 'constants/Feeds.constants';
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
import { EntityTabs, EntityType } from 'enums/entity.enum';
import { TagSource } from 'generated/type/tagLabel';
import { noop } from 'lodash';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
@ -127,11 +128,13 @@ const TopicVersion: FC<TopicVersionProp> = ({
<TopicSchemaFields
defaultExpandAllRows
isReadOnly
entityFieldThreads={[]}
entityFqn={currentVersionData?.fullyQualifiedName ?? ''}
hasDescriptionEditAccess={false}
hasTagEditAccess={false}
messageSchema={messageSchemaDiff}
showSchemaDisplayTypeSwitch={false}
onThreadLinkSelect={noop}
/>
</Col>
</Row>
@ -143,7 +146,6 @@ const TopicVersion: FC<TopicVersionProp> = ({
<Space className="w-full" direction="vertical" size="large">
{Object.keys(TagSource).map((tagType) => (
<TagsContainerV2
isVersionView
entityFqn={currentVersionData.fullyQualifiedName}
entityType={EntityType.TOPIC}
key={tagType}

View File

@ -71,6 +71,9 @@ export enum EntityField {
EXTENSION = 'extension',
DISPLAYNAME = 'displayName',
NAME = 'name',
MESSAGE_SCHEMA = 'messageSchema',
CHARTS = 'charts',
DATA_MODEL = 'dataModel',
}
export const ANNOUNCEMENT_BG = '#FFFDF8';

View File

@ -109,6 +109,7 @@ export enum FqnPart {
Table,
Column,
NestedColumn,
Topic,
}
export enum EntityInfo {

View File

@ -25,7 +25,15 @@ export interface EntityFieldThreadCount {
entityLink: string;
}
export type EntityThreadField = 'description' | 'columns' | 'tags' | 'tasks';
export type EntityThreadField =
| 'description'
| 'columns'
| 'tags'
| 'tasks'
| 'charts'
| 'dataModel'
| 'mlFeatures'
| 'messageSchema';
export interface EntityFieldThreads {
entityLink: string;
count: number;

View File

@ -124,9 +124,6 @@ const ContainerPage = () => {
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
const [threadLink, setThreadLink] = useState<string>('');
const [threadType, setThreadType] = useState<ThreadType>(
@ -238,6 +235,7 @@ const ContainerPage = () => {
isUserFollowing,
tags,
tier,
entityFqn,
} = useMemo(() => {
return {
deleted: containerData?.deleted,
@ -255,6 +253,7 @@ const ContainerPage = () => {
size: containerData?.size || 0,
numberOfObjects: containerData?.numberOfObjects || 0,
partitioned: containerData?.dataModel?.isPartitioned,
entityFqn: containerData?.fullyQualifiedName ?? '',
};
}, [containerData]);
@ -269,7 +268,6 @@ const ContainerPage = () => {
EntityType.CONTAINER,
containerName,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
@ -603,9 +601,15 @@ const ContainerPage = () => {
<ContainerDataModel
dataModel={containerData?.dataModel}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DATA_MODEL,
entityFieldThreadCount
)}
entityFqn={entityFqn}
hasDescriptionEditAccess={hasEditDescriptionPermission}
hasTagEditAccess={hasEditTagsPermission}
isReadOnly={Boolean(deleted)}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={handleUpdateDataModel}
/>
</div>
@ -748,7 +752,6 @@ const ContainerPage = () => {
entityFieldThreadCount,
tags,
entityLineage,
entityFieldTaskCount,
feedCount,
containerChildrenData,
handleAddLineage,

View File

@ -312,6 +312,7 @@ const DashboardDetailsPage = () => {
charts={charts}
createThread={createThread}
dashboardDetails={dashboardDetails}
fetchDashboard={() => fetchDashboardDetail(dashboardFQN)}
followDashboardHandler={followDashboard}
unFollowDashboardHandler={unFollowDashboard}
versionHandler={versionHandler}

View File

@ -321,6 +321,7 @@ const DataModelsPage = () => {
createThread={createThread}
dataModelData={dataModelData}
dataModelPermissions={dataModelPermissions}
fetchDataModel={() => fetchDataModelDetails(dashboardDataModelFQN)}
handleColumnUpdateDataModel={handleColumnUpdateDataModel}
handleFollowDataModel={handleFollowDataModel}
handleUpdateDescription={handleUpdateDescription}

View File

@ -291,6 +291,7 @@ const MlModelPage = () => {
<MlModelDetailComponent
createThread={createThread}
descriptionUpdateHandler={descriptionUpdateHandler}
fetchMlModel={() => fetchMlModelDetails(mlModelFqn)}
followMlModelHandler={followMlModel}
mlModelDetail={mlModelDetail}
settingsUpdateHandler={settingsUpdateHandler}

View File

@ -259,6 +259,7 @@ const PipelineDetailsPage = () => {
return (
<PipelineDetails
descriptionUpdateHandler={descriptionUpdateHandler}
fetchPipeline={() => fetchPipelineDetail(pipelineFQN)}
followPipelineHandler={followPipeline}
followers={followers}
paging={paging}

View File

@ -103,9 +103,6 @@ const TableDetailsPageV1 = () => {
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
const [isEdit, setIsEdit] = useState(false);
const [threadLink, setThreadLink] = useState<string>('');
const [threadType, setThreadType] = useState<ThreadType>(
@ -281,7 +278,6 @@ const TableDetailsPageV1 = () => {
EntityType.TABLE,
datasetFQN,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
@ -460,10 +456,6 @@ const TableDetailsPageV1 = () => {
FQN_SEPARATOR_CHAR
)}
columns={tableDetails?.columns ?? []}
entityFieldTasks={getEntityFieldThreadCounts(
EntityField.COLUMNS,
entityFieldTaskCount
)}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.COLUMNS,
entityFieldThreadCount

View File

@ -0,0 +1,106 @@
/*
* 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.
*/
import { Space, Tooltip } from 'antd';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { EntityField } from 'constants/Feeds.constants';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { getPartialNameFromTableFQN } from 'utils/CommonUtils';
import { ENTITY_LINK_SEPARATOR } from 'utils/EntityUtils';
import { getFieldThreadElement } from 'utils/FeedElementUtils';
import {
getEntityTaskDetails,
getRequestDescriptionPath,
getUpdateDescriptionPath,
} from 'utils/TasksUtils';
import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg';
import { EntityTaskDescriptionProps } from './entityTaskDescription.interface';
const EntityTaskDescription = ({
entityFqn,
entityType,
data,
onThreadLinkSelect,
entityFieldThreads,
}: EntityTaskDescriptionProps) => {
const history = useHistory();
const { t } = useTranslation();
const { fqnPart, entityField } = useMemo(
() => getEntityTaskDetails(entityType),
[entityType]
);
const columnName = useMemo(() => {
const columnName = getPartialNameFromTableFQN(data.fqn ?? '', fqnPart);
return columnName.includes(FQN_SEPARATOR_CHAR)
? `"${columnName}"`
: columnName;
}, [data.fqn]);
const handleDescriptionTask = (hasDescription: boolean) => {
history.push(
(hasDescription ? getUpdateDescriptionPath : getRequestDescriptionPath)(
entityType,
entityFqn,
entityField,
columnName
)
);
};
const requestDescriptionElement = useMemo(() => {
const hasDescription = Boolean(data?.description);
return (
<Tooltip
destroyTooltipOnHide
title={
hasDescription
? t('message.request-update-description')
: t('message.request-description')
}>
<IconRequest
className="cursor-pointer hover-cell-icon"
data-testid="request-description"
height={14}
name={t('message.request-description')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
onClick={() => handleDescriptionTask(hasDescription)}
/>
</Tooltip>
);
}, [data]);
return (
<Space size="middle">
{requestDescriptionElement}
{getFieldThreadElement(
columnName,
EntityField.DESCRIPTION,
entityFieldThreads,
onThreadLinkSelect,
entityType,
entityFqn,
`${entityField}${ENTITY_LINK_SEPARATOR}${columnName}${ENTITY_LINK_SEPARATOR}${EntityField.DESCRIPTION}`
)}
</Space>
);
};
export default EntityTaskDescription;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2022 Collate.
* 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
@ -11,13 +11,17 @@
* limitations under the License.
*/
.hover-icon-group {
.hover-cell-icon {
opacity: 0;
}
&:hover {
.hover-cell-icon {
opacity: 100;
}
}
import { EntityType } from 'enums/entity.enum';
import { ThreadType } from 'generated/api/feed/createThread';
import { EntityFieldThreads } from 'interface/feed.interface';
export interface EntityTaskDescriptionProps {
data: {
fqn?: string;
description?: string;
};
entityFqn: string;
entityType: EntityType;
entityFieldThreads: EntityFieldThreads[];
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}

View File

@ -0,0 +1,113 @@
/*
* 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.
*/
import { Space, Tooltip } from 'antd';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { EntityField } from 'constants/Feeds.constants';
import { TagSource } from 'generated/type/tagLabel';
import { isEmpty } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { getPartialNameFromTableFQN } from 'utils/CommonUtils';
import { ENTITY_LINK_SEPARATOR } from 'utils/EntityUtils';
import { getFieldThreadElement } from 'utils/FeedElementUtils';
import {
getEntityTaskDetails,
getRequestTagsPath,
getUpdateTagsPath,
} from 'utils/TasksUtils';
import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg';
import { EntityTaskTagsProps } from './EntityTaskTags.interface';
const EntityTaskTags = ({
data,
tagSource,
entityFqn,
entityType,
entityFieldThreads,
onThreadLinkSelect,
}: EntityTaskTagsProps) => {
const { t } = useTranslation();
const history = useHistory();
const { fqnPart, entityField } = useMemo(
() => getEntityTaskDetails(entityType),
[entityType]
);
const columnName = useMemo(() => {
const columnName = getPartialNameFromTableFQN(data.fqn ?? '', fqnPart);
return columnName.includes(FQN_SEPARATOR_CHAR)
? `"${columnName}"`
: columnName;
}, [data.fqn]);
const handleTagTask = (hasTags: boolean) => {
history.push(
(hasTags ? getUpdateTagsPath : getRequestTagsPath)(
entityType,
entityFqn,
entityField,
columnName
)
);
};
const getRequestTagsElement = useMemo(() => {
const hasTags = !isEmpty(data.tags);
return (
<Tooltip
destroyTooltipOnHide
overlayClassName="ant-popover-request-description"
title={
hasTags
? t('label.update-request-tag-plural')
: t('label.request-tag-plural')
}>
<IconRequest
className="hover-cell-icon cursor-pointer"
data-testid="request-tags"
height={14}
name={t('label.request-tag-plural')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
onClick={() => handleTagTask(hasTags)}
/>
</Tooltip>
);
}, [data]);
return (
<Space size="middle">
{/* Request and Update tags */}
{tagSource === TagSource.Classification && getRequestTagsElement}
{/* List Conversation */}
{getFieldThreadElement(
columnName,
EntityField.TAGS,
entityFieldThreads,
onThreadLinkSelect,
entityType,
entityFqn,
`${entityField}${ENTITY_LINK_SEPARATOR}${columnName}${ENTITY_LINK_SEPARATOR}${EntityField.TAGS}`
)}
</Space>
);
};
export default EntityTaskTags;

View File

@ -0,0 +1,29 @@
/*
* 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.
*/
import { EntityType } from 'enums/entity.enum';
import { ThreadType } from 'generated/api/feed/createThread';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
import { EntityFieldThreads } from 'interface/feed.interface';
export interface EntityTaskTagsProps {
data: {
fqn: string;
tags: TagLabel[];
};
tagSource: TagSource;
entityFqn: string;
entityType: EntityType;
entityFieldThreads: EntityFieldThreads[];
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}

View File

@ -34,7 +34,6 @@ import {
TaskType,
ThreadType,
} from '../../../generated/api/feed/createThread';
import { Table } from '../../../generated/entity/data/table';
import {
ENTITY_LINK_SEPARATOR,
getEntityFeedLink,
@ -45,6 +44,7 @@ import {
fetchOptions,
getBreadCrumbList,
getColumnObject,
getEntityColumnsDetails,
} from '../../../utils/TasksUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import Assignees from '../shared/Assignees';
@ -86,8 +86,12 @@ const UpdateDescription = () => {
const columnObject = useMemo(() => {
const column = getSanitizeValue.split(FQN_SEPARATOR_CHAR).slice(-1);
return getColumnObject(column[0], (entityData as Table).columns || []);
}, [field, entityData as Table]);
return getColumnObject(
column[0],
getEntityColumnsDetails(entityType, entityData),
entityType as EntityType
);
}, [field, entityData, entityType]);
const getDescription = () => {
if (!isEmpty(columnObject) && !isUndefined(columnObject)) {

View File

@ -19,6 +19,7 @@ import ResizablePanels from 'components/common/ResizablePanels/ResizablePanels';
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
import ExploreSearchCard from 'components/ExploreV1/ExploreSearchCard/ExploreSearchCard';
import { SearchedDataProps } from 'components/searched-data/SearchedData.interface';
import { Chart } from 'generated/entity/data/chart';
import { isEmpty, isUndefined } from 'lodash';
import { observer } from 'mobx-react';
import React, { useEffect, useMemo, useState } from 'react';
@ -34,7 +35,6 @@ import {
CreateThread,
TaskType,
} from '../../../generated/api/feed/createThread';
import { Table } from '../../../generated/entity/data/table';
import { ThreadType } from '../../../generated/entity/feed/thread';
import { TagLabel } from '../../../generated/type/tagLabel';
import {
@ -47,6 +47,7 @@ import {
fetchOptions,
getBreadCrumbList,
getColumnObject,
getEntityColumnsDetails,
} from '../../../utils/TasksUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import Assignees from '../shared/Assignees';
@ -67,6 +68,8 @@ const UpdateTag = () => {
const value = queryParams.get('value');
const [entityData, setEntityData] = useState<EntityData>({} as EntityData);
const [chartData, setChartData] = useState([] as Chart[]);
const [options, setOptions] = useState<Option[]>([]);
const [assignees, setAssignees] = useState<Option[]>([]);
const [currentTags, setCurrentTags] = useState<TagLabel[]>([]);
@ -89,8 +92,13 @@ const UpdateTag = () => {
const columnObject = useMemo(() => {
const column = getSanitizeValue.split(FQN_SEPARATOR_CHAR).slice(-1);
return getColumnObject(column[0], (entityData as Table).columns || []);
}, [field, entityData]);
return getColumnObject(
column[0],
getEntityColumnsDetails(entityType, entityData),
entityType as EntityType,
chartData
);
}, [field, entityData, chartData, entityType]);
const getTags = () => {
if (!isEmpty(columnObject) && !isUndefined(columnObject)) {
@ -151,7 +159,8 @@ const UpdateTag = () => {
fetchEntityDetail(
entityType as EntityType,
entityFQN as string,
setEntityData
setEntityData,
setChartData
);
}, [entityFQN, entityType]);
@ -174,7 +183,7 @@ const UpdateTag = () => {
updatedTags: getTags(),
assignees: defaultAssignee,
});
}, [entityData]);
}, [entityData, columnObject]);
useEffect(() => {
setCurrentTags(getTags());

View File

@ -237,6 +237,7 @@ const TopicDetailsPage: FunctionComponent = () => {
return (
<TopicDetails
createThread={createThread}
fetchTopic={() => fetchTopicDetail(topicFQN)}
followTopicHandler={followTopic}
topicDetails={topicDetails}
topicPermissions={topicPermissions}

View File

@ -325,6 +325,18 @@ a[href].link-text-grey,
}
}
.hover-icon-group {
.hover-cell-icon {
opacity: 0;
transition: 0.3s ease-in;
}
&:hover {
.hover-cell-icon {
opacity: 100;
}
}
}
/* Group CSS End*/
.quick-filter-dropdown-trigger-btn {

View File

@ -72,7 +72,7 @@ import {
import { SIZE } from '../enums/common.enum';
import { EntityTabs, EntityType, FqnPart } from '../enums/entity.enum';
import { FilterPatternEnum } from '../enums/filterPattern.enum';
import { ThreadTaskStatus, ThreadType } from '../generated/entity/feed/thread';
import { ThreadType } from '../generated/entity/feed/thread';
import { PipelineType } from '../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { EntityReference } from '../generated/entity/teams/user';
import { Paging } from '../generated/type/paging';
@ -144,6 +144,12 @@ export const getPartialNameFromTableFQN = (
return splitFqn.slice(4).join(FQN_SEPARATOR_CHAR);
}
if (fqnParts.includes(FqnPart.Topic)) {
// Remove the first 2 parts ( service, database)
return splitFqn.slice(2).join(FQN_SEPARATOR_CHAR);
}
const arrPartialName = [];
if (splitFqn.length > 0) {
if (fqnParts.includes(FqnPart.Service)) {
@ -551,7 +557,6 @@ export const getFeedCounts = (
conversationCallback: (
value: React.SetStateAction<EntityFieldThreadCount[]>
) => void,
taskCallback: (value: React.SetStateAction<EntityFieldThreadCount[]>) => void,
entityCallback: (value: React.SetStateAction<number>) => void
) => {
// To get conversation count
@ -570,23 +575,6 @@ export const getFeedCounts = (
showErrorToast(err, t('server.entity-feed-fetch-error'));
});
// To get open tasks count
getFeedCount(
getEntityFeedLink(entityType, entityFQN),
ThreadType.Task,
ThreadTaskStatus.Open
)
.then((res) => {
if (res) {
taskCallback(res.counts);
} else {
throw t('server.entity-feed-fetch-error');
}
})
.catch((err: AxiosError) => {
showErrorToast(err, t('server.entity-feed-fetch-error'));
});
// To get all thread count (task + conversation)
getFeedCount(getEntityFeedLink(entityType, entityFQN))
.then((res) => {
@ -977,7 +965,3 @@ export const getEntityDetailLink = (
return path;
};
export const getPartialNameFromTopicFQN = (fqn: string): string => {
return Fqn.split(fqn).slice(2).join(FQN_SEPARATOR_CHAR);
};

View File

@ -10,6 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TabSpecificField } from 'enums/entity.enum';
import { Column, ContainerDataModel } from 'generated/entity/data/container';
import { LabelType, State, TagLabel } from 'generated/type/tagLabel';
import { isEmpty } from 'lodash';
@ -94,3 +95,6 @@ export const updateContainerColumnDescription = (
}
});
};
export const ContainerFields = `${TabSpecificField.TAGS}, ${TabSpecificField.OWNER},
${TabSpecificField.FOLLOWERS},${TabSpecificField.DATAMODEL}`;

View File

@ -11,13 +11,12 @@
* limitations under the License.
*/
import { Button, Space, Tooltip } from 'antd';
import { Tooltip } from 'antd';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { t } from 'i18next';
import { isEmpty, isEqual, isUndefined } from 'lodash';
import React, { Fragment } from 'react';
import { isEmpty, isUndefined } from 'lodash';
import React from 'react';
import { ReactComponent as IconComments } from '../assets/svg/comment.svg';
import { ReactComponent as IconTaskColor } from '../assets/svg/Task-ic.svg';
import { entityUrlMap } from '../constants/Feeds.constants';
import { ThreadType } from '../generated/entity/feed/thread';
import { EntityReference } from '../generated/entity/teams/user';
@ -36,12 +35,10 @@ export const getFieldThreadElement = (
columnName: string,
columnField: string,
entityFieldThreads: EntityFieldThreads[],
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void,
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void,
entityType?: string,
entityFqn?: string,
entityField?: string,
flag = true,
threadType?: ThreadType
entityField?: string
) => {
let threadValue: EntityFieldThreads = {} as EntityFieldThreads;
@ -52,61 +49,31 @@ export const getFieldThreadElement = (
}
});
const isTaskType = isEqual(threadType, ThreadType.Task);
return !isEmpty(threadValue) ? (
<Button
className="link-text tw-self-start w-8 h-7 m-r-xss d-flex tw-items-center hover-cell-icon p-0"
data-testid="field-thread"
type="text"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onThreadLinkSelect?.(
threadValue.entityLink,
isTaskType ? ThreadType.Task : ThreadType.Conversation
);
}}>
<Tooltip
destroyTooltipOnHide
overlayClassName="ant-popover-request-description"
title={t('label.list-entity', {
entity: isTaskType ? t('label.task') : t('label.conversation'),
})}>
<Space align="center" className="w-full h-full" size={4}>
{isTaskType ? (
<IconTaskColor {...iconsProps} />
) : (
<IconComments {...iconsProps} />
)}
</Space>
</Tooltip>
</Button>
) : (
<Fragment>
{entityType && entityFqn && entityField && flag && !isTaskType ? (
<Button
className="link-text tw-self-start w-7 h-7 m-r-xss flex-none hover-cell-icon p-0"
data-testid="start-field-thread"
type="text"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onThreadLinkSelect?.(
getEntityFeedLink(entityType, entityFqn, entityField)
);
}}>
<Tooltip
destroyTooltipOnHide
overlayClassName="ant-popover-request-description"
title={t('label.start-entity', {
entity: t('label.conversation'),
})}>
<IconComments {...iconsProps} />
</Tooltip>
</Button>
) : null}
</Fragment>
return (
<Tooltip
destroyTooltipOnHide
overlayClassName="ant-popover-request-description"
title={t('label.list-entity', {
entity: t('label.conversation'),
})}>
<IconComments
{...iconsProps}
className="hover-cell-icon cursor-pointer"
data-testid="field-thread"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
isEmpty(threadValue)
? onThreadLinkSelect(
getEntityFeedLink(entityType, entityFqn, entityField)
)
: onThreadLinkSelect(
threadValue.entityLink,
ThreadType.Conversation
);
}}
/>
</Tooltip>
);
};

View File

@ -13,7 +13,15 @@
import { AxiosError } from 'axios';
import { ActivityFeedTabs } from 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface';
import { EntityField } from 'constants/Feeds.constants';
import { Change, diffWordsWithSpace } from 'diff';
import { Chart } from 'generated/entity/data/chart';
import { Container } from 'generated/entity/data/container';
import { Dashboard } from 'generated/entity/data/dashboard';
import { MlFeature, Mlmodel } from 'generated/entity/data/mlmodel';
import { Pipeline, Task } from 'generated/entity/data/pipeline';
import { Field, Topic } from 'generated/entity/data/topic';
import { TagLabel } from 'generated/type/tagLabel';
import i18Next from 'i18next';
import { isEqual, isUndefined } from 'lodash';
import {
@ -49,7 +57,11 @@ import { ServiceCategory } from '../enums/service.enum';
import { Column, Table } from '../generated/entity/data/table';
import { TaskType, Thread } from '../generated/entity/feed/thread';
import { getEntityDetailLink, getPartialNameFromTableFQN } from './CommonUtils';
import { defaultFields as DashboardFields } from './DashboardDetailsUtils';
import { ContainerFields } from './ContainerDetailUtils';
import {
defaultFields as DashboardFields,
fetchCharts,
} from './DashboardDetailsUtils';
import { defaultFields as DatabaseSchemaFields } from './DatabaseSchemaDetailsUtils';
import { defaultFields as DataModelFields } from './DataModelsUtils';
import { defaultFields as TableFields } from './DatasetDetailsUtils';
@ -178,19 +190,66 @@ export const fetchOptions = (
.catch((err: AxiosError) => showErrorToast(err));
};
export const getEntityColumnsDetails = (
entityType: string,
entityData: EntityData
) => {
switch (entityType) {
case EntityType.TOPIC:
return (entityData as Topic).messageSchema?.schemaFields ?? [];
case EntityType.DASHBOARD:
return (entityData as Dashboard).charts ?? [];
case EntityType.PIPELINE:
return (entityData as Pipeline).tasks ?? [];
case EntityType.MLMODEL:
return (entityData as Mlmodel).mlFeatures ?? [];
case EntityType.CONTAINER:
return (entityData as Container).dataModel?.columns ?? [];
default:
return (entityData as Table).columns ?? [];
}
};
type EntityColumns = Column[] | Task[] | MlFeature[] | Field[];
interface EntityColumnProps {
description: string;
tags: TagLabel[];
}
export const getColumnObject = (
columnName: string,
columns: Table['columns']
): Column => {
let columnObject: Column = {} as Column;
columns: EntityColumns,
entityType: EntityType,
chartData?: Chart[]
): EntityColumnProps => {
let columnObject: EntityColumnProps = {} as EntityColumnProps;
for (let index = 0; index < columns.length; index++) {
const column = columns[index];
if (isEqual(column.name, columnName)) {
columnObject = column;
columnObject = {
description: column.description ?? '',
tags:
column.tags ??
(entityType === EntityType.DASHBOARD
? chartData?.find((item) => item.name === columnName)?.tags ?? []
: []),
};
break;
} else {
columnObject = getColumnObject(columnName, column.children || []);
columnObject = getColumnObject(
columnName,
(column as Column).children || [],
entityType,
chartData
);
}
}
@ -298,7 +357,8 @@ export const getBreadCrumbList = (
export const fetchEntityDetail = (
entityType: EntityType,
entityFQN: string,
setEntityData: (value: React.SetStateAction<EntityData>) => void
setEntityData: (value: React.SetStateAction<EntityData>) => void,
setChartData?: (value: React.SetStateAction<Chart[]>) => void
) => {
switch (entityType) {
case EntityType.TABLE:
@ -321,6 +381,11 @@ export const fetchEntityDetail = (
getDashboardByFqn(entityFQN, DashboardFields)
.then((res) => {
setEntityData(res);
fetchCharts(res.charts)
.then((chart) => {
setChartData?.(chart);
})
.catch((err: AxiosError) => showErrorToast(err));
})
.catch((err: AxiosError) => showErrorToast(err));
@ -361,7 +426,7 @@ export const fetchEntityDetail = (
break;
case EntityType.CONTAINER:
getContainerByFQN(entityFQN, DataModelFields)
getContainerByFQN(entityFQN, ContainerFields)
.then((res) => {
setEntityData(res);
})
@ -401,3 +466,56 @@ export const isDescriptionTask = (taskType: TaskType) =>
export const isTagsTask = (taskType: TaskType) =>
[TaskType.RequestTag, TaskType.UpdateTag].includes(taskType);
export const getEntityTaskDetails = (
entityType: EntityType
): {
fqnPart: FqnPart[];
entityField: string;
} => {
let fqnPartTypes: FqnPart;
let entityField: string;
switch (entityType) {
case EntityType.TABLE:
fqnPartTypes = FqnPart.NestedColumn;
entityField = EntityField.COLUMNS;
break;
case EntityType.TOPIC:
fqnPartTypes = FqnPart.Topic;
entityField = EntityField.MESSAGE_SCHEMA;
break;
case EntityType.DASHBOARD:
fqnPartTypes = FqnPart.Database;
entityField = EntityField.CHARTS;
break;
case EntityType.PIPELINE:
fqnPartTypes = FqnPart.Schema;
entityField = EntityField.TASKS;
break;
case EntityType.MLMODEL:
fqnPartTypes = FqnPart.Schema;
entityField = EntityField.ML_FEATURES;
break;
case EntityType.CONTAINER:
fqnPartTypes = FqnPart.Topic;
entityField = EntityField.DATA_MODEL;
break;
default:
fqnPartTypes = FqnPart.Table;
entityField = EntityField.COLUMNS;
}
return { fqnPart: [fqnPartTypes], entityField };
};