Feat (#7561) Announcements are not supported for MLModel from UI (#8109)

* Feat (#7561) Announcements are not supported for MLModel from UI

* Fix unit test

* Use Async/Await pattern

* Add conversation support for mlModel

* Add task support for mlModel

* Fix task page loading issue

* Fix unit test

* Add End to end test for entity announcement

* Fix Add CreateAnnouncement function issue

* Add End to End Test for entity task

* Fix End to End Test for Announcement and Tasks

* Revert "Fix End to End Test for Announcement and Tasks"

This reverts commit 48cbc0b6158b352b9e19e8290ff52a47849bb648.

* Fix Description unit test

* Fix cypress test

* Fix cypress test

* Fix entity task cypress test

* Remove EntityTask Spec

* Addressing review comment

* Addressing review comment

* Addressing review comment
This commit is contained in:
Sachin Chaurasiya 2022-10-17 18:47:17 +05:30 committed by GitHub
parent 3faef0e0cf
commit 786be3bedf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 797 additions and 307 deletions

View File

@ -263,6 +263,8 @@ export const LOGIN = {
password: 'admin',
};
export const ANNOUNCEMENT_ENTITIES = [SEARCH_ENTITY_TABLE.table_1, SEARCH_ENTITY_TOPIC.topic_1, SEARCH_ENTITY_DASHBOARD.dashboard_1, SEARCH_ENTITY_PIPELINE.pipeline_1]
export const HTTP_CONFIG_SOURCE = {
DBT_CATALOG_HTTP_PATH:
'https://raw.githubusercontent.com/OnkarVO7/dbt_git_test/master/catalog.json',

View File

@ -0,0 +1,91 @@
/*
* Copyright 2021 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 { getCurrentLocaleDate, getFutureLocaleDateFromCurrentDate } from "../../../src/utils/TimeUtils";
import { descriptionBox, login, visitEntityDetailsPage } from "../../common/common";
import { ANNOUNCEMENT_ENTITIES, LOGIN } from "../../constants/constants";
describe("Entity Announcement", () => {
beforeEach(() => {
login(LOGIN.username, LOGIN.password);
cy.goToHomePage();
});
const createAnnouncement = (title, startDate, endDate, description) => {
cy.get('[data-testid="add-announcement"]').should('be.visible').click();
cy.get('.ant-modal-header')
.should('be.visible')
.contains('Make an announcement');
cy.get('.ant-modal-body').should('be.visible');
cy.get('#title').should('be.visible').type(title);
cy.get('#startDate').should('be.visible').type(startDate);
cy.get('#endtDate').should('be.visible').type(endDate);
cy.get(descriptionBox).type(description);
cy.get('.ant-modal-footer > .ant-btn-primary')
.should('be.visible')
.contains('Submit')
.scrollIntoView()
.click();
}
const addAnnouncement = (value) => {
const startDate = getCurrentLocaleDate();
const endDate = getFutureLocaleDateFromCurrentDate(5);
visitEntityDetailsPage(value.term, value.serviceName, value.entity);
cy.get('[data-testid="manage-button"]').should('be.visible').click();
cy.get('[data-testid="announcement-button"]').should('be.visible').click();
cy.get('[data-testid="announcement-error"]')
.should('be.visible')
.contains('No Announcements, Click on add announcement to add one.');
// Create Active Announcement
createAnnouncement("Announcement Title", startDate, endDate, "Announcement Description")
// wait time for success toast message
cy.wait(5000);
// Create InActive Announcement
const InActiveStartDate = getFutureLocaleDateFromCurrentDate(6);
const InActiveEndDate = getFutureLocaleDateFromCurrentDate(11);
createAnnouncement("InActive Announcement Title",InActiveStartDate,InActiveEndDate,"InActive Announcement Description")
// wait time for success toast message
cy.wait(5000);
// check for inActive-announcement
cy.get('[data-testid="inActive-announcements"]').should('be.visible');
// close announcement drawer
cy.get('.anticon > svg').should('be.visible').click();
// reload page to get the active announcement card
cy.reload();
// check for announcement card on entity page
cy.get('[data-testid="announcement-card"]').should('be.visible');
};
ANNOUNCEMENT_ENTITIES.forEach((entity) => {
it(`Add announcement and verify the active announcement for ${entity.entity}`, () => {
addAnnouncement(entity)
})
})
})

View File

@ -171,7 +171,9 @@ const AnnouncementThreads: FC<ActivityThreadListProp> = ({
{getAnnouncements(activeAnnouncements)}
{Boolean(inActiveAnnouncements.length) && (
<>
<Typography.Text className="tw-block tw-mt-4 tw-font-medium">
<Typography.Text
className="tw-block tw-mt-4 tw-font-medium"
data-testid="inActive-announcements">
Inactive Announcements
</Typography.Text>
<Divider className="tw-mb-4 tw-mt-2" />

View File

@ -16,6 +16,7 @@ import { LeafNodes } from 'Models';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Mlmodel } from '../../generated/entity/data/mlmodel';
import { Paging } from '../../generated/type/paging';
import MlModelDetailComponent from './MlModelDetail.component';
const mockData = {
@ -153,6 +154,18 @@ const mockProp = {
isNodeLoading: { id: undefined, state: false },
},
onExtensionUpdate: jest.fn(),
entityThread: [],
isEntityThreadLoading: false,
paging: {} as Paging,
feedCount: 2,
fetchFeedHandler: jest.fn(),
postFeedHandler: jest.fn(),
deletePostHandler: jest.fn(),
updateThreadHandler: jest.fn(),
entityFieldThreadCount: [],
entityFieldTaskCount: [],
createThread: jest.fn(),
};
jest.mock('../common/description/Description', () => {
@ -179,6 +192,14 @@ jest.mock('../common/TabsPane/TabsPane', () => {
return jest.fn().mockReturnValue(<p data-testid="tabs">Tabs</p>);
});
jest.mock('../ActivityFeed/ActivityFeedList/ActivityFeedList.tsx', () => {
return jest.fn().mockReturnValue(<p>ActivityFeedList</p>);
});
jest.mock('../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel', () => {
return jest.fn().mockReturnValue(<p>ActivityThreadPanel</p>);
});
jest.mock('../../utils/CommonUtils', () => {
return {
getEntityName: jest.fn().mockReturnValue('entityName'),
@ -224,7 +245,7 @@ describe('Test MlModel entity detail component', () => {
it('Should render hyper parameter and ml store table for details tab', async () => {
const { container } = render(
<MlModelDetailComponent {...mockProp} activeTab={2} />,
<MlModelDetailComponent {...mockProp} activeTab={3} />,
{
wrapper: MemoryRouter,
}
@ -245,7 +266,7 @@ describe('Test MlModel entity detail component', () => {
it('Should render lineage tab', async () => {
const { container } = render(
<MlModelDetailComponent {...mockProp} activeTab={3} />,
<MlModelDetailComponent {...mockProp} activeTab={4} />,
{
wrapper: MemoryRouter,
}
@ -258,7 +279,7 @@ describe('Test MlModel entity detail component', () => {
it('Check if active tab is custom properties', async () => {
const { container } = render(
<MlModelDetailComponent {...mockProp} activeTab={4} />,
<MlModelDetailComponent {...mockProp} activeTab={5} />,
{
wrapper: MemoryRouter,
}

View File

@ -11,22 +11,16 @@
* limitations under the License.
*/
import { Table } from 'antd';
import { Col, Row, Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import classNames from 'classnames';
import { isUndefined, startCase, uniqueId } from 'lodash';
import { observer } from 'mobx-react';
import {
EntityTags,
ExtraInfo,
LeafNodes,
LineagePos,
LoadingNodeState,
} from 'Models';
import { EntityTags, ExtraInfo } from 'Models';
import React, {
FC,
Fragment,
HTMLAttributes,
RefObject,
useCallback,
useEffect,
useMemo,
@ -38,24 +32,31 @@ import {
getDashboardDetailsPath,
getServiceDetailsPath,
} from '../../constants/constants';
import { EntityField } from '../../constants/feed.constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType } from '../../enums/entity.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { OwnerType } from '../../enums/user.enum';
import { MlHyperParameter } from '../../generated/api/data/createMlModel';
import { Mlmodel } from '../../generated/entity/data/mlmodel';
import { EntityLineage } from '../../generated/type/entityLineage';
import { ThreadType } from '../../generated/entity/feed/thread';
import { EntityReference } from '../../generated/type/entityReference';
import { Paging } from '../../generated/type/paging';
import { LabelType, State, TagLabel } from '../../generated/type/tagLabel';
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll';
import jsonData from '../../jsons/en';
import {
getEntityName,
getEntityPlaceHolder,
getOwnerValue,
} from '../../utils/CommonUtils';
import { getEntityFieldThreadCounts } from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList';
import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
import { CustomPropertyTable } from '../common/CustomPropertyTable/CustomPropertyTable';
import { CustomPropertyProps } from '../common/CustomPropertyTable/CustomPropertyTable.interface';
import Description from '../common/description/Description';
@ -64,34 +65,12 @@ import TabsPane from '../common/TabsPane/TabsPane';
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
import PageContainer from '../containers/PageContainer';
import EntityLineageComponent from '../EntityLineage/EntityLineage.component';
import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface';
import Loader from '../Loader/Loader';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
import { MlModelDetailProp } from './MlModelDetail.interface';
import MlModelFeaturesList from './MlModelFeaturesList';
interface MlModelDetailProp extends HTMLAttributes<HTMLDivElement> {
mlModelDetail: Mlmodel;
activeTab: number;
followMlModelHandler: () => void;
unfollowMlModelHandler: () => void;
descriptionUpdateHandler: (updatedMlModel: Mlmodel) => Promise<void>;
setActiveTabHandler: (value: number) => void;
tagUpdateHandler: (updatedMlModel: Mlmodel) => void;
updateMlModelFeatures: (updatedMlModel: Mlmodel) => Promise<void>;
settingsUpdateHandler: (updatedMlModel: Mlmodel) => Promise<void>;
lineageTabData: {
loadNodeHandler: (node: EntityReference, pos: LineagePos) => void;
addLineageHandler: (edge: Edge) => Promise<void>;
removeLineageHandler: (data: EdgeData) => void;
entityLineageHandler: (lineage: EntityLineage) => void;
isLineageLoading?: boolean;
entityLineage: EntityLineage;
lineageLeafNodes: LeafNodes;
isNodeLoading: LoadingNodeState;
};
onExtensionUpdate: (updatedMlModel: Mlmodel) => Promise<void>;
}
const MlModelDetail: FC<MlModelDetailProp> = ({
mlModelDetail,
activeTab,
@ -104,6 +83,17 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
updateMlModelFeatures,
lineageTabData,
onExtensionUpdate,
entityThread,
isEntityThreadLoading,
fetchFeedHandler,
deletePostHandler,
postFeedHandler,
updateThreadHandler,
paging,
feedCount,
createThread,
entityFieldTaskCount,
entityFieldThreadCount,
}) => {
const [followersCount, setFollowersCount] = useState<number>(0);
const [isFollowing, setIsFollowing] = useState<boolean>(false);
@ -114,6 +104,11 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
DEFAULT_ENTITY_PERMISSION
);
const [threadType, setThreadType] = useState<ThreadType>(
ThreadType.Conversation
);
const [threadLink, setThreadLink] = useState<string>('');
const { getEntityPermission } = usePermissionProvider();
const fetchResourcePermission = useCallback(async () => {
@ -130,6 +125,8 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
}
}, [mlModelDetail.id, getEntityPermission, setPipelinePermissions]);
const [elementRef, isInView] = useInfiniteScroll(observerOptions);
useEffect(() => {
if (mlModelDetail.id) {
fetchResourcePermission();
@ -232,6 +229,12 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
isProtected: false,
position: 1,
},
{
name: 'Activity Feeds & Tasks',
isProtected: false,
position: 2,
count: feedCount,
},
{
name: 'Details',
icon: {
@ -241,17 +244,17 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
selectedName: 'icon-detailscolor',
},
isProtected: false,
position: 2,
position: 3,
},
{
name: 'Lineage',
isProtected: false,
position: 3,
position: 4,
},
{
name: 'Custom Properties',
isProtected: false,
position: 4,
position: 5,
},
];
@ -341,6 +344,17 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
await updateMlModelFeatures({ ...mlModelDetail, mlFeatures: features });
};
const handleThreadLinkSelect = (link: string, threadType?: ThreadType) => {
setThreadLink(link);
if (threadType) {
setThreadType(threadType);
}
};
const handleThreadPanelClose = () => {
setThreadLink('');
};
const getMlHyperParametersColumn: ColumnsType<MlHyperParameter> = useMemo(
() => [
{
@ -430,6 +444,27 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
);
};
const fetchMoreThread = (
isElementInView: boolean,
pagingObj: Paging,
isLoading: boolean
) => {
if (isElementInView && pagingObj?.after && !isLoading) {
fetchFeedHandler(pagingObj.after);
}
};
const handleFeedFilterChange = useCallback(
(feedType, threadType) => {
fetchFeedHandler(paging.after, feedType, threadType);
},
[paging]
);
useEffect(() => {
fetchMoreThread(isInView as boolean, paging, isEntityThreadLoading);
}, [paging, isEntityThreadLoading, isInView]);
useEffect(() => {
setFollowersData(mlModelDetail.followers || []);
}, [
@ -446,6 +481,14 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
<EntityPageInfo
canDelete={mlModelPermissions.Delete}
deleted={mlModelDetail.deleted}
entityFieldTasks={getEntityFieldThreadCounts(
EntityField.TAGS,
entityFieldTaskCount
)}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.TAGS,
entityFieldThreadCount
)}
entityFqn={mlModelDetail.fullyQualifiedName}
entityId={mlModelDetail.id}
entityName={mlModelDetail.name}
@ -472,6 +515,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
? onTierUpdate
: undefined
}
onThreadLinkSelect={handleThreadLinkSelect}
/>
<div className="tw-mt-4 tw-flex tw-flex-col tw-flex-grow">
@ -487,6 +531,14 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
<Fragment>
<Description
description={mlModelDetail.description}
entityFieldTasks={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldTaskCount
)}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldThreadCount
)}
entityFqn={mlModelDetail.fullyQualifiedName}
entityName={mlModelDetail.name}
entityType={EntityType.MLMODEL}
@ -500,6 +552,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
onThreadLinkSelect={handleThreadLinkSelect}
/>
<MlModelFeaturesList
handleFeaturesUpdate={onFeaturesUpdate}
@ -509,12 +562,28 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
</Fragment>
)}
{activeTab === 2 && (
<Row id="activityfeed">
<Col offset={3} span={18}>
<ActivityFeedList
isEntityFeed
withSidePanel
deletePostHandler={deletePostHandler}
entityName={mlModelDetail.name}
feedList={entityThread}
postFeedHandler={postFeedHandler}
updateThreadHandler={updateThreadHandler}
onFeedFiltersUpdate={handleFeedFilterChange}
/>
</Col>
</Row>
)}
{activeTab === 3 && (
<div className="tw-grid tw-grid-cols-2 tw-gap-x-6">
{getMlHyperParameters()}
{getMlModelStore()}
</div>
)}
{activeTab === 3 && (
{activeTab === 4 && (
<div
className="tw-px-2 tw-h-full"
data-testid="lineage-details">
@ -536,7 +605,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
/>
</div>
)}
{activeTab === 4 && (
{activeTab === 5 && (
<CustomPropertyTable
entityDetails={
mlModelDetail as CustomPropertyProps['entityDetails']
@ -545,10 +614,28 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
handleExtentionUpdate={onExtensionUpdate}
/>
)}
<div
data-testid="observer-element"
id="observer-element"
ref={elementRef as RefObject<HTMLDivElement>}>
{isEntityThreadLoading ? <Loader /> : null}
</div>
</div>
</div>
</div>
</div>
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
deletePostHandler={deletePostHandler}
open={Boolean(threadLink)}
postFeedHandler={postFeedHandler}
threadLink={threadLink}
threadType={threadType}
updateThreadHandler={updateThreadHandler}
onCancel={handleThreadPanelClose}
/>
) : null}
</PageContainer>
);
};

View File

@ -0,0 +1,72 @@
/*
* Copyright 2021 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 {
EntityFieldThreadCount,
EntityReference,
LeafNodes,
LineagePos,
LoadingNodeState,
} from 'Models';
import { HTMLAttributes } from 'react';
import { FeedFilter } from '../../enums/mydata.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
import { Mlmodel } from '../../generated/entity/data/mlmodel';
import { Thread, ThreadType } from '../../generated/entity/feed/thread';
import { EntityLineage } from '../../generated/type/entityLineage';
import { Paging } from '../../generated/type/paging';
import { ThreadUpdatedFunc } from '../../interface/feed.interface';
import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface';
export interface MlModelDetailProp extends HTMLAttributes<HTMLDivElement> {
mlModelDetail: Mlmodel;
activeTab: number;
entityThread: Thread[];
isEntityThreadLoading: boolean;
paging: Paging;
feedCount: number;
followMlModelHandler: () => void;
unfollowMlModelHandler: () => void;
descriptionUpdateHandler: (updatedMlModel: Mlmodel) => Promise<void>;
setActiveTabHandler: (value: number) => void;
tagUpdateHandler: (updatedMlModel: Mlmodel) => void;
updateMlModelFeatures: (updatedMlModel: Mlmodel) => Promise<void>;
settingsUpdateHandler: (updatedMlModel: Mlmodel) => Promise<void>;
lineageTabData: {
loadNodeHandler: (node: EntityReference, pos: LineagePos) => void;
addLineageHandler: (edge: Edge) => Promise<void>;
removeLineageHandler: (data: EdgeData) => void;
entityLineageHandler: (lineage: EntityLineage) => void;
isLineageLoading?: boolean;
entityLineage: EntityLineage;
lineageLeafNodes: LeafNodes;
isNodeLoading: LoadingNodeState;
};
onExtensionUpdate: (updatedMlModel: Mlmodel) => Promise<void>;
fetchFeedHandler: (
after?: string,
feedType?: FeedFilter,
threadType?: ThreadType
) => void;
postFeedHandler: (value: string, id: string) => void;
deletePostHandler: (
threadId: string,
postId: string,
isThread: boolean
) => void;
updateThreadHandler: ThreadUpdatedFunc;
entityFieldThreadCount: EntityFieldThreadCount[];
entityFieldTaskCount: EntityFieldThreadCount[];
createThread: (data: CreateThread) => void;
}

View File

@ -35,6 +35,7 @@ const AssigneeList: FC<Props> = ({ assignees, className }) => {
userName={assignee.name || ''}>
<span
className="tw-flex tw-m-1.5 tw-mt-0 tw-cursor-pointer"
data-testid="assignee"
onClick={(e) => {
e.stopPropagation();
history.push(getUserPath(assignee.name ?? ''));

View File

@ -223,7 +223,7 @@ describe('Test Description Component', () => {
const requestDescription = await findByTestId(
container,
'request-description'
'request-entity-description'
);
expect(descriptionContainer).toBeInTheDocument();

View File

@ -27,6 +27,7 @@ import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import {
getRequestDescriptionPath,
getUpdateDescriptionPath,
TASK_ENTITIES,
} from '../../../utils/TasksUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
@ -46,7 +47,6 @@ const Description: FC<DescriptionProps> = ({
entityName,
entityFieldThreads,
onThreadLinkSelect,
onEntityFieldSelect,
entityType,
entityFqn,
entityFieldTasks,
@ -87,10 +87,10 @@ const Description: FC<DescriptionProps> = ({
const RequestDescriptionEl = () => {
const hasDescription = Boolean(description.trim());
return onEntityFieldSelect ? (
return TASK_ENTITIES.includes(entityType as EntityType) ? (
<button
className="tw-w-7 tw-h-7 tw-flex-none link-text focus:tw-outline-none"
data-testid="request-description"
data-testid="request-entity-description"
onClick={
hasDescription ? handleUpdateDescription : handleRequestDescription
}>

View File

@ -297,8 +297,7 @@ const EntityPageInfo = ({
const getThreadElements = () => {
if (!isUndefined(entityFieldThreads)) {
return !isUndefined(tagThread) &&
TASK_ENTITIES.includes(entityType as EntityType) ? (
return !isUndefined(tagThread) ? (
<button
className="tw-w-7 tw-h-7 tw-flex-none link-text focus:tw-outline-none"
data-testid="tag-thread"
@ -337,10 +336,11 @@ const EntityPageInfo = ({
const hasTags = !isEmpty(tags);
const text = hasTags ? 'Update request tags' : 'Request tags';
return onThreadLinkSelect ? (
return onThreadLinkSelect &&
TASK_ENTITIES.includes(entityType as EntityType) ? (
<button
className="tw-w-7 tw-h-7 tw-mr-1 tw-flex-none link-text focus:tw-outline-none tw-align-top"
data-testid="request-description"
data-testid="request-entity-tags"
onClick={hasTags ? handleUpdateTags : handleRequestTags}>
<Popover
destroyTooltipOnHide

View File

@ -12,12 +12,23 @@
*/
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { compare, Operation } from 'fast-json-patch';
import { isEmpty, isNil } from 'lodash';
import { observer } from 'mobx-react';
import { LeafNodes, LineagePos, LoadingNodeState } from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import {
EntityFieldThreadCount,
LeafNodes,
LineagePos,
LoadingNodeState,
} from 'Models';
import React, { Fragment, useEffect, useMemo, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import AppState from '../../AppState';
import {
getAllFeeds,
postFeedById,
postThread,
} from '../../axiosAPIs/feedsAPI';
import { getLineageByFQN } from '../../axiosAPIs/lineageAPI';
import { addLineage, deleteLineageEdge } from '../../axiosAPIs/miscAPI';
import {
@ -38,17 +49,23 @@ import { ResourceEntity } from '../../components/PermissionProvider/PermissionPr
import { getMlModelPath } from '../../constants/constants';
import { NO_PERMISSION_TO_VIEW } from '../../constants/HelperTextUtil';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { FeedFilter } from '../../enums/mydata.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
import { Mlmodel } from '../../generated/entity/data/mlmodel';
import { Post, Thread, ThreadType } from '../../generated/entity/feed/thread';
import {
EntityLineage,
EntityReference,
} from '../../generated/type/entityLineage';
import { Paging } from '../../generated/type/paging';
import jsonData from '../../jsons/en';
import {
getCurrentUserId,
getEntityMissingError,
getFeedCounts,
} from '../../utils/CommonUtils';
import { getEntityLineage } from '../../utils/EntityUtils';
import { getEntityFeedLink, getEntityLineage } from '../../utils/EntityUtils';
import { deletePost, updateThreadData } from '../../utils/FeedUtils';
import {
defaultFields,
getCurrentMlModelTab,
@ -79,6 +96,25 @@ const MlModelPage = () => {
DEFAULT_ENTITY_PERMISSION
);
const [entityThread, setEntityThread] = useState<Thread[]>([]);
const [isEntityThreadLoading, setIsEntityThreadLoading] =
useState<boolean>(false);
const [paging, setPaging] = useState<Paging>({} as Paging);
const [feedCount, setFeedCount] = useState<number>(0);
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
// get current user details
const currentUser = useMemo(
() => AppState.getCurrentUserDetails(),
[AppState.userDetails, AppState.nonSecureUserDetails]
);
const { getEntityPermissionByFqn } = usePermissionProvider();
const fetchResourcePermission = async (entityFqn: string) => {
@ -100,7 +136,7 @@ const MlModelPage = () => {
const getLineageData = () => {
setIsLineageLoading(true);
getLineageByFQN(mlModelDetail.fullyQualifiedName ?? '', EntityType.MLMODEL)
getLineageByFQN(mlModelFqn, EntityType.MLMODEL)
.then((res) => {
if (res) {
setEntityLineage(res);
@ -202,10 +238,57 @@ const MlModelPage = () => {
}
};
const fetchEntityFeedCount = () => {
getFeedCounts(
EntityType.MLMODEL,
mlModelFqn,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
const fetchFeedData = async (
after?: string,
feedType?: FeedFilter,
threadType?: ThreadType
) => {
try {
setIsEntityThreadLoading(true);
const response = await getAllFeeds(
getEntityFeedLink(EntityType.MLMODEL, mlModelFqn),
after,
threadType,
feedType,
undefined,
USERId
);
const { data, paging: pagingObj } = response;
setPaging(pagingObj);
setEntityThread((prevData) => [...prevData, ...data]);
} catch (error) {
showErrorToast(
error as AxiosError,
jsonData['api-error-messages']['fetch-entity-feed-error']
);
} finally {
setIsEntityThreadLoading(false);
}
};
const handleFeedFetchFromFeedList = (
after?: string,
feedType?: FeedFilter,
threadType?: ThreadType
) => {
!after && setEntityThread([]);
fetchFeedData(after, feedType, threadType);
};
const fetchTabSpecificData = (tabField = '') => {
switch (tabField) {
case TabSpecificField.LINEAGE: {
if (!isEmpty(mlModelDetail) && !mlModelDetail.deleted) {
if (!mlModelDetail.deleted) {
if (isEmpty(entityLineage)) {
getLineageData();
}
@ -215,6 +298,11 @@ const MlModelPage = () => {
break;
}
case TabSpecificField.ACTIVITY_FEED: {
fetchFeedData();
break;
}
default:
break;
@ -391,13 +479,77 @@ const MlModelPage = () => {
}
};
const postFeedHandler = async (value: string, threadId: string) => {
const data = {
message: value,
from: currentUser?.name,
} as Post;
try {
const response = await postFeedById(threadId, data);
const { id, posts } = response;
setEntityThread((pre) => {
return pre.map((thread) => {
if (thread.id === id) {
return { ...response, posts: posts?.slice(-3) };
} else {
return thread;
}
});
});
fetchEntityFeedCount();
} catch (error) {
showErrorToast(
error as AxiosError,
jsonData['api-error-messages']['add-feed-error']
);
}
};
const createThread = async (data: CreateThread) => {
try {
const response = await postThread(data);
setEntityThread((pre) => [...pre, response]);
fetchEntityFeedCount();
} catch (error) {
showErrorToast(
error as AxiosError,
jsonData['api-error-messages']['create-conversation-error']
);
}
};
const deletePostHandler = (
threadId: string,
postId: string,
isThread: boolean
) => {
deletePost(threadId, postId, isThread, setEntityThread);
};
const updateThreadHandler = (
threadId: string,
postId: string,
isThread: boolean,
data: Operation[]
) => {
updateThreadData(threadId, postId, isThread, data, setEntityThread);
};
const getMlModelDetail = () => {
if (!isNil(mlModelDetail) && !isEmpty(mlModelDetail)) {
return (
<MlModelDetailComponent
activeTab={activeTab}
createThread={createThread}
deletePostHandler={deletePostHandler}
descriptionUpdateHandler={descriptionUpdateHandler}
entityFieldTaskCount={entityFieldTaskCount}
entityFieldThreadCount={entityFieldThreadCount}
entityThread={entityThread}
feedCount={feedCount}
fetchFeedHandler={handleFeedFetchFromFeedList}
followMlModelHandler={followMlModel}
isEntityThreadLoading={isEntityThreadLoading}
lineageTabData={{
loadNodeHandler,
addLineageHandler,
@ -409,11 +561,14 @@ const MlModelPage = () => {
isNodeLoading,
}}
mlModelDetail={mlModelDetail}
paging={paging}
postFeedHandler={postFeedHandler}
setActiveTabHandler={activeTabHandler}
settingsUpdateHandler={settingsUpdateHandler}
tagUpdateHandler={onTagUpdate}
unfollowMlModelHandler={unfollowMlModel}
updateMlModelFeatures={updateMlModelFeatures}
updateThreadHandler={updateThreadHandler}
onExtensionUpdate={handleExtentionUpdate}
/>
);
@ -426,13 +581,18 @@ const MlModelPage = () => {
}
};
useEffect(() => {
setEntityThread([]);
}, [tab]);
useEffect(() => {
fetchTabSpecificData(mlModelTabs[activeTab - 1].field);
}, [activeTab, mlModelDetail]);
}, [activeTab]);
useEffect(() => {
if (mlModelPermissions.ViewAll || mlModelPermissions.ViewBasic) {
fetchMlModelDetails(mlModelFqn);
fetchEntityFeedCount();
}
}, [mlModelPermissions, mlModelFqn]);

View File

@ -230,7 +230,7 @@ const RequestTag = () => {
Back
</Button>
<Button
data-testid="submit-test"
data-testid="submit-tag-request"
htmlType="submit"
type="primary">
{suggestion ? 'Suggest' : 'Submit'}

View File

@ -41,6 +41,7 @@ import ErrorPlaceHolder from '../../../components/common/error-with-placeholder/
import UserPopOverCard from '../../../components/common/PopOverCard/UserPopOverCard';
import ProfilePicture from '../../../components/common/ProfilePicture/ProfilePicture';
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
import Loader from '../../../components/Loader/Loader';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { PanelTab, TaskOperation } from '../../../constants/feed.constants';
import { EntityType } from '../../../enums/entity.enum';
@ -116,6 +117,7 @@ const TaskDetailPage = () => {
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [comment, setComment] = useState<string>('');
const [tagsSuggestion, setTagsSuggestion] = useState<TagLabel[]>([]);
const [isTaskLoading, setIsTaskLoading] = useState<boolean>(false);
// get current user details
const currentUser = useMemo(
@ -144,7 +146,7 @@ const TaskDetailPage = () => {
const columnName = columnValue.split(FQN_SEPARATOR_CHAR).pop();
return getColumnObject(
columnName as string,
columnName ?? '',
(entityData as Table).columns || []
);
}, [taskDetail, entityData]);
@ -173,14 +175,16 @@ const TaskDetailPage = () => {
const isTaskTags = isTagsTask(taskDetail.task?.type as TaskType);
const fetchTaskDetail = () => {
getTask(taskId)
.then((res) => {
setTaskDetail(res);
})
.catch((err: AxiosError) => {
showErrorToast(err, '', 5000, setError);
});
const fetchTaskDetail = async () => {
setIsTaskLoading(true);
try {
const data = await getTask(taskId);
setTaskDetail(data);
} catch (error) {
showErrorToast(error as AxiosError, '', 5000, setError);
} finally {
setIsTaskLoading(false);
}
};
const fetchTaskFeed = (id: string) => {
@ -309,8 +313,8 @@ const TaskDetailPage = () => {
showSuccessToast('Task Resolved Successfully');
history.push(
getEntityLink(
entityType as string,
entityData?.fullyQualifiedName as string
entityType ?? '',
entityData?.fullyQualifiedName ?? ''
)
);
})
@ -345,8 +349,8 @@ const TaskDetailPage = () => {
showSuccessToast('Task Closed Successfully');
history.push(
getEntityLink(
entityType as string,
entityData?.fullyQualifiedName as string
entityType ?? '',
entityData?.fullyQualifiedName ?? ''
)
);
})
@ -458,7 +462,7 @@ const TaskDetailPage = () => {
entityFQN &&
fetchEntityDetail(
entityType as EntityType,
getEncodedFqn(entityFQN as string),
getEncodedFqn(entityFQN ?? ''),
setEntityData
);
fetchTaskFeed(taskDetail.id);
@ -467,9 +471,9 @@ const TaskDetailPage = () => {
const taskAssignees = taskDetail.task?.assignees || [];
if (taskAssignees.length) {
const assigneesArr = taskAssignees.map((assignee) => ({
label: assignee.name as string,
label: assignee.name ?? '',
value: assignee.id,
type: assignee.type as string,
type: assignee.type ?? '',
}));
setAssignees(assigneesArr);
setOptions(assigneesArr);
@ -506,252 +510,262 @@ const TaskDetailPage = () => {
isAdminUser || isAuthDisabled || isAssignee || isOwner;
return (
<Layout style={{ ...background, height: '100vh' }}>
{error ? (
<ErrorPlaceHolder>{error}</ErrorPlaceHolder>
<>
{isTaskLoading ? (
<Loader />
) : (
<Fragment>
<Content style={{ ...contentStyles, overflowY: 'auto' }}>
<TitleBreadcrumb
titleLinks={[
...getBreadCrumbList(entityData, entityType as EntityType),
{
name: `Task #${taskDetail.task?.id}`,
activeTitle: true,
url: '',
},
]}
/>
<EntityDetail entityData={entityData} />
<Card
data-testid="task-metadata"
style={{ ...cardStyles, marginTop: '16px' }}>
<p
className="tw-text-base tw-font-medium tw-mb-4"
data-testid="task-title">
{`Task #${taskId}`} {taskDetail.message}
</p>
<div className="tw-flex tw-mb-4" data-testid="task-metadata">
<TaskStatus
status={taskDetail.task?.status as ThreadTaskStatus}
<Layout style={{ ...background, height: '100vh' }}>
{error ? (
<ErrorPlaceHolder>{error}</ErrorPlaceHolder>
) : (
<Fragment>
<Content style={{ ...contentStyles, overflowY: 'auto' }}>
<TitleBreadcrumb
titleLinks={[
...getBreadCrumbList(entityData, entityType as EntityType),
{
name: `Task #${taskDetail.task?.id}`,
activeTitle: true,
url: '',
},
]}
/>
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">
|
</span>
<span className="tw-flex">
<UserPopOverCard userName={taskDetail.createdBy || ''}>
<EntityDetail entityData={entityData} />
<Card
data-testid="task-metadata"
style={{ ...cardStyles, marginTop: '16px' }}>
<p
className="tw-text-base tw-font-medium tw-mb-4"
data-testid="task-title">
{`Task #${taskId}`} {taskDetail.message}
</p>
<div className="tw-flex tw-mb-4" data-testid="task-metadata">
<TaskStatus
status={taskDetail.task?.status as ThreadTaskStatus}
/>
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">
|
</span>
<span className="tw-flex">
<ProfilePicture
displayName={taskDetail.createdBy || ''}
id=""
name={taskDetail.createdBy || ''}
width="20"
/>
<span className="tw-font-semibold tw-cursor-pointer hover:tw-underline tw-ml-1">
{taskDetail.createdBy}
<UserPopOverCard userName={taskDetail.createdBy || ''}>
<span className="tw-flex">
<ProfilePicture
displayName={taskDetail.createdBy || ''}
id=""
name={taskDetail.createdBy || ''}
width="20"
/>
<span className="tw-font-semibold tw-cursor-pointer hover:tw-underline tw-ml-1">
{taskDetail.createdBy}
</span>
</span>
</UserPopOverCard>
<span className="tw-ml-1">created this task </span>
<span className="tw-ml-1">
{toLower(
getDayTimeByTimeStamp(taskDetail.threadTs ?? 0)
)}
</span>
</span>
</UserPopOverCard>
<span className="tw-ml-1">created this task </span>
<span className="tw-ml-1">
{toLower(
getDayTimeByTimeStamp(taskDetail.threadTs as number)
)}
</span>
</span>
</div>
</div>
<ColumnDetail column={columnObject} />
<div className="tw-flex" data-testid="task-assignees">
<span
className={classNames('tw-text-grey-muted', {
'tw-self-center tw-mr-2': editAssignee,
})}>
Assignees:
</span>
{editAssignee ? (
<Fragment>
<Assignees
assignees={assignees}
options={options}
onChange={setAssignees}
onSearch={onSearch}
/>
<Button
className="tw-mx-1 tw-self-center ant-btn-primary-custom"
size="small"
type="primary"
onClick={() => setEditAssignee(false)}>
<FontAwesomeIcon
className="tw-w-3.5 tw-h-3.5"
icon="times"
/>
</Button>
<Button
className="tw-mx-1 tw-self-center ant-btn-primary-custom"
disabled={!assignees.length}
size="small"
type="primary"
onClick={onTaskUpdate}>
<FontAwesomeIcon
className="tw-w-3.5 tw-h-3.5"
icon="check"
/>
</Button>
</Fragment>
) : (
<Fragment>
<AssigneeList
assignees={taskDetail?.task?.assignees || []}
className="tw-ml-0.5 tw-align-middle tw-inline-flex tw-flex-wrap"
/>
{(hasEditAccess() || isCreator) && !isTaskClosed && (
<button
className="focus:tw-outline-none tw-self-baseline tw-flex-none"
data-testid="edit-suggestion"
onClick={() => setEditAssignee(true)}>
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="14px"
<ColumnDetail column={columnObject} />
<div className="tw-flex" data-testid="task-assignees">
<span
className={classNames('tw-text-grey-muted', {
'tw-self-center tw-mr-2': editAssignee,
})}>
Assignees:
</span>
{editAssignee ? (
<Fragment>
<Assignees
assignees={assignees}
options={options}
onChange={setAssignees}
onSearch={onSearch}
/>
</button>
)}
</Fragment>
)}
</div>
</Card>
<Card
data-testid="task-data"
style={{ ...cardStyles, marginTop: '16px', marginLeft: '24px' }}>
{isTaskDescription && (
<DescriptionTask
currentDescription={currentDescription()}
hasEditAccess={hasEditAccess()}
isTaskActionEdit={isTaskActionEdit}
suggestion={suggestion}
taskDetail={taskDetail}
onSuggestionChange={onSuggestionChange}
/>
)}
{isTaskTags && (
<TagsTask
currentTags={getCurrentTags()}
hasEditAccess={hasEditAccess()}
isTaskActionEdit={isTaskActionEdit}
setSuggestion={setTagsSuggestion}
suggestions={tagsSuggestion}
task={taskDetail.task}
/>
)}
<div
className="tw-flex tw-justify-end"
data-testid="task-cta-buttons">
{(hasEditAccess() || isCreator) && !isTaskClosed && (
<Button
className="ant-btn-link-custom"
type="link"
onClick={() => setModalVisible(true)}>
Close with comment
</Button>
)}
{hasEditAccess() && !isTaskClosed && (
<Fragment>
{taskDetail.task?.suggestion ? (
<Dropdown.Button
className="ant-btn-primary-dropdown"
icon={
<Button
className="tw-mx-1 tw-self-center ant-btn-primary-custom"
size="small"
type="primary"
onClick={() => setEditAssignee(false)}>
<FontAwesomeIcon
className="tw-text-sm"
icon={faChevronDown}
className="tw-w-3.5 tw-h-3.5"
icon="times"
/>
}
overlay={
<Menu
selectable
items={TASK_ACTION_LIST}
selectedKeys={[taskAction.key]}
onClick={(info) => onTaskActionChange(info.key)}
</Button>
<Button
className="tw-mx-1 tw-self-center ant-btn-primary-custom"
disabled={!assignees.length}
size="small"
type="primary"
onClick={onTaskUpdate}>
<FontAwesomeIcon
className="tw-w-3.5 tw-h-3.5"
icon="check"
/>
}
trigger={['click']}
type="primary"
onClick={onTaskResolve}>
{taskAction.label}
</Dropdown.Button>
</Button>
</Fragment>
) : (
<Fragment>
<AssigneeList
assignees={taskDetail?.task?.assignees || []}
className="tw-ml-0.5 tw-align-middle tw-inline-flex tw-flex-wrap"
/>
{(hasEditAccess() || isCreator) && !isTaskClosed && (
<button
className="focus:tw-outline-none tw-self-baseline tw-flex-none"
data-testid="edit-suggestion"
onClick={() => setEditAssignee(true)}>
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="14px"
/>
</button>
)}
</Fragment>
)}
</div>
</Card>
<Card
className="mt-4 ml-6"
data-testid="task-data"
style={{
...cardStyles,
}}>
{isTaskDescription && (
<DescriptionTask
currentDescription={currentDescription()}
hasEditAccess={hasEditAccess()}
isTaskActionEdit={isTaskActionEdit}
suggestion={suggestion}
taskDetail={taskDetail}
onSuggestionChange={onSuggestionChange}
/>
)}
{isTaskTags && (
<TagsTask
currentTags={getCurrentTags()}
hasEditAccess={hasEditAccess()}
isTaskActionEdit={isTaskActionEdit}
setSuggestion={setTagsSuggestion}
suggestions={tagsSuggestion}
task={taskDetail.task}
/>
)}
<div
className="tw-flex tw-justify-end"
data-testid="task-cta-buttons">
{(hasEditAccess() || isCreator) && !isTaskClosed && (
<Button
className="ant-btn-primary-custom"
disabled={!suggestion}
type="primary"
onClick={onTaskResolve}>
Add Description
className="ant-btn-link-custom"
type="link"
onClick={() => setModalVisible(true)}>
Close with comment
</Button>
)}
</Fragment>
)}
</div>
{isTaskClosed && <ClosedTask task={taskDetail.task} />}
</Card>
<CommentModal
comment={comment}
isVisible={modalVisible}
setComment={setComment}
taskDetail={taskDetail}
onClose={onCommentModalClose}
onConfirm={onTaskReject}
/>
</Content>
<Sider
className="ant-layout-sider-task-detail"
data-testid="task-right-sider"
theme="light"
width={600}>
<Tabs className="ant-tabs-custom-line" onChange={onTabChange}>
<TabPane key={PanelTab.TASKS} tab="Task">
{!isEmpty(taskFeedDetail) ? (
<div id="task-feed">
<FeedPanelBody
isLoading={isLoading}
threadData={taskFeedDetail}
updateThreadHandler={onTaskFeedUpdate}
/>
<ActivityFeedEditor
buttonClass="tw-mr-4"
className="tw-ml-5 tw-mr-2 tw-mb-2"
onSave={onPostTaskFeed}
/>
{hasEditAccess() && !isTaskClosed && (
<Fragment>
{taskDetail.task?.suggestion ? (
<Dropdown.Button
className="ant-btn-primary-dropdown"
data-testid="complete-task"
icon={
<FontAwesomeIcon
className="tw-text-sm"
icon={faChevronDown}
/>
}
overlay={
<Menu
selectable
items={TASK_ACTION_LIST}
selectedKeys={[taskAction.key]}
onClick={(info) => onTaskActionChange(info.key)}
/>
}
trigger={['click']}
type="primary"
onClick={onTaskResolve}>
{taskAction.label}
</Dropdown.Button>
) : (
<Button
className="ant-btn-primary-custom"
disabled={!suggestion}
type="primary"
onClick={onTaskResolve}>
Add Description
</Button>
)}
</Fragment>
)}
</div>
) : null}
</TabPane>
<TabPane key={PanelTab.CONVERSATIONS} tab="Conversations">
{!isEmpty(taskFeedDetail) ? (
<ActivityThreadPanelBody
className="tw-p-0"
createThread={createThread}
deletePostHandler={deletePostHandler}
postFeedHandler={postFeedHandler}
showHeader={false}
threadLink={taskFeedDetail.about}
threadType={ThreadType.Conversation}
updateThreadHandler={updateThreadHandler}
/>
) : null}
</TabPane>
</Tabs>
</Sider>
</Fragment>
{isTaskClosed && <ClosedTask task={taskDetail.task} />}
</Card>
<CommentModal
comment={comment}
isVisible={modalVisible}
setComment={setComment}
taskDetail={taskDetail}
onClose={onCommentModalClose}
onConfirm={onTaskReject}
/>
</Content>
<Sider
className="ant-layout-sider-task-detail"
data-testid="task-right-sider"
theme="light"
width={600}>
<Tabs className="ant-tabs-custom-line" onChange={onTabChange}>
<TabPane key={PanelTab.TASKS} tab="Task">
{!isEmpty(taskFeedDetail) ? (
<div id="task-feed">
<FeedPanelBody
isLoading={isLoading}
threadData={taskFeedDetail}
updateThreadHandler={onTaskFeedUpdate}
/>
<ActivityFeedEditor
buttonClass="tw-mr-4"
className="tw-ml-5 tw-mr-2 tw-mb-2"
onSave={onPostTaskFeed}
/>
</div>
) : null}
</TabPane>
<TabPane key={PanelTab.CONVERSATIONS} tab="Conversations">
{!isEmpty(taskFeedDetail) ? (
<ActivityThreadPanelBody
className="tw-p-0"
createThread={createThread}
deletePostHandler={deletePostHandler}
postFeedHandler={postFeedHandler}
showHeader={false}
threadLink={taskFeedDetail.about}
threadType={ThreadType.Conversation}
updateThreadHandler={updateThreadHandler}
/>
) : null}
</TabPane>
</Tabs>
</Sider>
</Fragment>
)}
</Layout>
)}
</Layout>
</>
);
};

View File

@ -12,11 +12,12 @@
*/
import { Dashboard } from '../../generated/entity/data/dashboard';
import { Mlmodel } from '../../generated/entity/data/mlmodel';
import { Pipeline } from '../../generated/entity/data/pipeline';
import { Table } from '../../generated/entity/data/table';
import { Topic } from '../../generated/entity/data/topic';
export type EntityData = Table | Topic | Dashboard | Pipeline;
export type EntityData = Table | Topic | Dashboard | Pipeline | Mlmodel;
export interface Option {
label: string;

View File

@ -45,3 +45,11 @@ window.ResizeObserver = jest.fn().mockImplementation(() => ({
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
/**
* mock implementation of IntersectionObserver
*/
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
}));

View File

@ -192,6 +192,12 @@
.mt-4 {
margin-top: 1rem;
}
.ml-4 {
margin-left: 1rem;
}
.ml-6 {
margin-left: 1.5rem;
}
.mt-8 {
margin-top: 2rem;
}

View File

@ -5,6 +5,7 @@ export const ANNOUNCEMENT_ENTITIES = [
EntityType.DASHBOARD,
EntityType.TOPIC,
EntityType.PIPELINE,
EntityType.MLMODEL,
];
export const validateMessages = {

View File

@ -21,6 +21,11 @@ export const mlModelTabs = [
name: 'Features',
path: 'features',
},
{
name: 'Activity Feed',
path: 'activity_feed',
field: TabSpecificField.ACTIVITY_FEED,
},
{
name: 'Details',
path: 'details',
@ -39,17 +44,21 @@ export const mlModelTabs = [
export const getCurrentMlModelTab = (tab: string) => {
let currentTab = 1;
switch (tab) {
case 'details':
case 'activity_feed':
currentTab = 2;
break;
case 'lineage':
case 'details':
currentTab = 3;
break;
case 'custom_properties':
case 'lineage':
currentTab = 4;
break;
case 'custom_properties':
currentTab = 5;
break;
case 'features':

View File

@ -16,6 +16,7 @@ import { Change, diffWordsWithSpace } from 'diff';
import { isEqual, isUndefined } from 'lodash';
import { getDashboardByFqn } from '../axiosAPIs/dashboardAPI';
import { getUserSuggestions } from '../axiosAPIs/miscAPI';
import { getMlModelByFQN } from '../axiosAPIs/mlModelAPI';
import { getPipelineByFqn } from '../axiosAPIs/pipelineAPI';
import { getTableDetailsByFQN } from '../axiosAPIs/tableAPI';
import { getTopicByFqn } from '../axiosAPIs/topicsAPI';
@ -40,6 +41,7 @@ import {
import { getEntityName, getPartialNameFromTableFQN } from './CommonUtils';
import { defaultFields as DashboardFields } from './DashboardDetailsUtils';
import { defaultFields as TableFields } from './DatasetDetailsUtils';
import { defaultFields as MlModelFields } from './MlModelDetailsUtils';
import { defaultFields as PipelineFields } from './PipelineDetailsUtils';
import { serviceTypeLogo } from './ServiceUtils';
import { getEntityLink } from './TableUtils';
@ -183,6 +185,7 @@ export const TASK_ENTITIES = [
EntityType.DASHBOARD,
EntityType.TOPIC,
EntityType.PIPELINE,
EntityType.MLMODEL,
];
export const getBreadCrumbList = (
@ -248,6 +251,10 @@ export const getBreadCrumbList = (
return [service(ServiceCategory.PIPELINE_SERVICES), activeEntity];
}
case EntityType.MLMODEL: {
return [service(ServiceCategory.ML_MODEL_SERVICES), activeEntity];
}
default:
return [];
}
@ -291,6 +298,14 @@ export const fetchEntityDetail = (
.catch((err: AxiosError) => showErrorToast(err));
break;
case EntityType.MLMODEL:
getMlModelByFQN(entityFQN, MlModelFields)
.then((res) => {
setEntityData(res);
})
.catch((err: AxiosError) => showErrorToast(err));
break;
default:
break;