mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-29 09:42:23 +00:00
* 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:
parent
3faef0e0cf
commit
786be3bedf
@ -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',
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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" />
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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 ?? ''));
|
||||
|
||||
@ -223,7 +223,7 @@ describe('Test Description Component', () => {
|
||||
|
||||
const requestDescription = await findByTestId(
|
||||
container,
|
||||
'request-description'
|
||||
'request-entity-description'
|
||||
);
|
||||
|
||||
expect(descriptionContainer).toBeInTheDocument();
|
||||
|
||||
@ -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
|
||||
}>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
}));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ export const ANNOUNCEMENT_ENTITIES = [
|
||||
EntityType.DASHBOARD,
|
||||
EntityType.TOPIC,
|
||||
EntityType.PIPELINE,
|
||||
EntityType.MLMODEL,
|
||||
];
|
||||
|
||||
export const validateMessages = {
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user