Assets improvements part 2 (#14255)

* fix: make filters consistent

* disable if no selection

* fix: hide notification

* fix: failed status data

* localization

* fix notification loading

* remove close icon here

* fix: cypress

* update error alert design

* fix alert styling

* fix: minor issues

* fix: redirect link of data product

* fix: minor suggestion prompt

* complete user flow

* fix: glossary cypress

* update alert view

* fix tags manipulation

* fix: domain reload fixes

Co-Authored-By: Ashish Gupta <ashish@getcollate.io>

* fix: minor loading issue on delete

* fix: add admin check for activity feed

Co-Authored-By: Ashish Gupta <ashish@getcollate.io>

* fix: minor css fixes

* fix: localisation

* fix: minor issue

* miner cypress fix

* add mlmodelservice as mlmodel parent

* fix: added null checks

---------

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
Co-authored-by: Ashish Gupta <ashish@getcollate.io>
Co-authored-by: Shailesh Parmar <shailesh.parmar.webdev@gmail.com>
Co-authored-by: 07Himank <himank07mehta@gmail.com>
This commit is contained in:
karanh37 2023-12-06 23:25:40 +05:30 committed by GitHub
parent a76308bf77
commit a215f03a28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 554 additions and 431 deletions

View File

@ -273,7 +273,8 @@ public class SearchRepository {
if (changeDescription != null) {
Pair<String, Map<String, Object>> updates = getInheritedFieldChanges(changeDescription);
Pair<String, String> parentMatch;
if (updates.getValue().get("type").toString().equalsIgnoreCase("domain")
if (!updates.getValue().isEmpty()
&& updates.getValue().get("type").toString().equalsIgnoreCase("domain")
&& (entityType.equalsIgnoreCase(Entity.DATABASE_SERVICE)
|| entityType.equalsIgnoreCase(Entity.DASHBOARD_SERVICE)
|| entityType.equalsIgnoreCase(Entity.MESSAGING_SERVICE)

View File

@ -207,7 +207,7 @@
"indexName": "mlmodel_service_search_index",
"indexMappingFile": "/elasticsearch/%s/mlmodel_service_index_mapping.json",
"alias": "mlModelService",
"parentAliases": ["all"]
"parentAliases": ["all","mlModelService"]
},
"testCaseResolutionStatus": {
"indexName": "test_case_resolution_status_search_index",

View File

@ -307,6 +307,7 @@ export const NEW_GLOSSARY = {
reviewer: 'Aaron Johnson',
addReviewer: true,
tag: 'PersonalData.Personal',
isMutually: true,
};
export const NEW_GLOSSARY_1 = {
name: 'Cypress Product%Glossary',

View File

@ -116,7 +116,11 @@ const createGlossary = (glossaryData) => {
.should('be.visible')
.type(glossaryData.description);
cy.get('[data-testid="mutually-exclusive-button"]').scrollIntoView().click();
if (glossaryData.isMutually) {
cy.get('[data-testid="mutually-exclusive-button"]')
.scrollIntoView()
.click();
}
if (glossaryData.tag) {
// Add tag
@ -311,10 +315,12 @@ const removeAssetsFromGlossaryTerm = (glossaryTerm, glossary) => {
cy.get('[data-testid="delete-button"]').click();
cy.get("[data-testid='save-button']").click();
interceptURL('GET', '/api/v1/search/query*', 'assetTab');
cy.get('[data-testid="overview"]').click();
cy.get('[data-testid="assets"]').click();
// go assets tab
goToAssetsTab(glossaryTerm.name, glossaryTerm.fullyQualifiedName, true);
verifyResponseStatusCode('@assetTab', 200);
verifyResponseStatusCode('@searchAssets', 200);
checkAssetsCount(glossaryTerm.assets.length - (index + 1));
});
};
@ -719,7 +725,7 @@ describe('Glossary page should work properly', () => {
const ProductTerms = Object.values(NEW_GLOSSARY_1_TERMS);
ProductTerms.forEach((term) =>
createGlossaryTerm(term, NEW_GLOSSARY_1, 'Approved')
createGlossaryTerm(term, NEW_GLOSSARY_1, 'Approved', false)
);
});

View File

@ -131,7 +131,7 @@ describe('Postgres Ingestion', () => {
cy.get('#root\\/filterCondition')
.scrollIntoView()
.type(`s.query like '%${tableName}%'`);
.type(`s.query like '%%${tableName}%%'`);
cy.get('[data-testid="submit-btn"]')
.scrollIntoView()
.should('be.visible')

View File

@ -40,6 +40,7 @@ import {
ThreadTaskStatus,
ThreadType,
} from '../../../generated/entity/feed/thread';
import { useAuth } from '../../../hooks/authHooks';
import { useElementInView } from '../../../hooks/useElementInView';
import { getAllFeeds, getFeedCount } from '../../../rest/feedsAPI';
import { getCountBadge, getEntityDetailLink } from '../../../utils/CommonUtils';
@ -81,6 +82,7 @@ export const ActivityFeedTab = ({
});
const { subTab: activeTab = ActivityFeedTabs.ALL } =
useParams<{ subTab: ActivityFeedTabs }>();
const { isAdminUser } = useAuth();
const [taskFilter, setTaskFilter] = useState<TaskFilter>('open');
const [allCount, setAllCount] = useState(0);
const [tasksCount, setTasksCount] = useState(0);
@ -162,7 +164,7 @@ export const ActivityFeedTab = ({
undefined,
undefined,
ThreadType.Task,
FeedFilter.OWNER,
isAdminUser ? undefined : FeedFilter.OWNER,
undefined,
userId
).then((res) => {
@ -178,7 +180,7 @@ export const ActivityFeedTab = ({
undefined,
undefined,
ThreadType.Conversation,
FeedFilter.OWNER,
isAdminUser ? undefined : FeedFilter.OWNER,
undefined,
userId
).then((res) => {

View File

@ -13,6 +13,7 @@
import { Typography } from 'antd';
import { AxiosError } from 'axios';
import { isEmpty } from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Loader from '../../components/Loader/Loader';
@ -239,7 +240,7 @@ const Suggestions = ({
return <Loader />;
}
if (options.length === 0 && !isTourOpen) {
if (options.length === 0 && !isTourOpen && !isEmpty(searchText)) {
return (
<Typography.Text>
<Transi18next

View File

@ -1,188 +0,0 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PlusOutlined } from '@ant-design/icons';
import { Button, Divider, Dropdown, Menu, Typography } from 'antd';
import { isEmpty } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SearchIndex } from '../../enums/search.enum';
import {
QueryFieldInterface,
QueryFieldValueInterface,
} from '../../pages/ExplorePage/ExplorePage.interface';
import { getAssetsPageQuickFilters } from '../../utils/AdvancedSearchUtils';
import { getSelectedValuesFromQuickFilter } from '../../utils/Explore.utils';
import { ExploreQuickFilterField } from '../Explore/ExplorePage.interface';
import ExploreQuickFilters from '../Explore/ExploreQuickFilters';
import { AssetFiltersProps } from './AssetFilters.interface';
const AssetFilters = ({
filterData,
type,
aggregations,
onQuickFilterChange,
quickFilterQuery,
}: AssetFiltersProps) => {
const { t } = useTranslation();
const [filters, setFilters] = useState<ExploreQuickFilterField[]>([]);
const [selectedFilter, setSelectedFilter] = useState<string[]>([]);
const [selectedQuickFilters, setSelectedQuickFilters] = useState<
ExploreQuickFilterField[]
>([]);
const handleMenuClick = ({ key }: { key: string }) => {
setSelectedFilter((prevSelected) => [...prevSelected, key]);
};
const menu = useMemo(
() => (
<Menu selectedKeys={selectedFilter} onClick={handleMenuClick}>
{filters.map((filter) => (
<Menu.Item key={filter.key}>{filter.label}</Menu.Item>
))}
</Menu>
),
[selectedFilter, filters]
);
const handleQuickFiltersChange = (data: ExploreQuickFilterField[]) => {
const must: QueryFieldInterface[] = [];
data.forEach((filter) => {
if (!isEmpty(filter.value)) {
const should: QueryFieldValueInterface[] = [];
if (filter.value) {
filter.value.forEach((filterValue) => {
const term: Record<string, string> = {};
term[filter.key] = filterValue.key;
should.push({ term });
});
}
must.push({
bool: { should },
});
}
});
const quickFilterQuery = isEmpty(must)
? undefined
: {
query: { bool: { must } },
};
onQuickFilterChange?.(quickFilterQuery);
};
const handleQuickFiltersValueSelect = useCallback(
(field: ExploreQuickFilterField) => {
setSelectedQuickFilters((pre) => {
const data = pre.map((preField) => {
if (preField.key === field.key) {
return field;
} else {
return preField;
}
});
handleQuickFiltersChange(data);
return data;
});
},
[setSelectedQuickFilters]
);
const clearFilters = useCallback(() => {
setSelectedQuickFilters((pre) => {
const data = pre.map((preField) => {
return { ...preField, value: [] };
});
handleQuickFiltersChange(data);
return data;
});
}, [handleQuickFiltersChange, setSelectedQuickFilters]);
useEffect(() => {
if (filterData?.length === 0) {
const dropdownItems = getAssetsPageQuickFilters(type);
setFilters(
dropdownItems.map((item) => ({
...item,
value: getSelectedValuesFromQuickFilter(item, dropdownItems),
}))
);
} else {
setFilters(filterData ?? []);
}
}, [filterData, type]);
useEffect(() => {
const updatedQuickFilters = filters
.filter((filter) => selectedFilter.includes(filter.key))
.map((selectedFilterItem) => {
const originalFilterItem = selectedQuickFilters?.find(
(filter) => filter.key === selectedFilterItem.key
);
return originalFilterItem || selectedFilterItem;
});
const newItems = updatedQuickFilters.filter(
(item) =>
!selectedQuickFilters.some(
(existingItem) => item.key === existingItem.key
)
);
if (newItems.length > 0) {
setSelectedQuickFilters((prevSelected) => [...prevSelected, ...newItems]);
}
}, [selectedFilter, selectedQuickFilters, filters]);
return (
<>
<Dropdown overlay={menu} trigger={['click']}>
<Button icon={<PlusOutlined />} size="small" type="primary" />
</Dropdown>
{selectedQuickFilters.length > 0 && (
<>
<Divider className="m-x-md h-6" type="vertical" />
<div className="d-flex justify-between flex-1">
<ExploreQuickFilters
aggregations={aggregations}
fields={selectedQuickFilters}
index={SearchIndex.ALL}
onFieldValueSelect={(data) =>
handleQuickFiltersValueSelect?.(data)
}
/>
{quickFilterQuery && (
<Typography.Text
className="text-primary self-center cursor-pointer"
onClick={clearFilters}>
{t('label.clear-entity', {
entity: '',
})}
</Typography.Text>
)}
</div>
</>
)}
</>
);
};
export default AssetFilters;

View File

@ -1,25 +0,0 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Aggregations } from '../../interface/search.interface';
import { QueryFilterInterface } from '../../pages/ExplorePage/ExplorePage.interface';
import { ExploreQuickFilterField } from '../Explore/ExplorePage.interface';
import { AssetsOfEntity } from '../Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
export interface AssetFiltersProps {
filterData?: ExploreQuickFilterField[];
defaultFilter?: string[];
aggregations?: Aggregations;
onQuickFilterChange?: (query?: QueryFilterInterface) => void;
type: AssetsOfEntity;
quickFilterQuery?: QueryFilterInterface;
}

View File

@ -10,9 +10,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Checkbox, List, Modal, Space, Typography } from 'antd';
import {
CheckOutlined,
CloseOutlined,
ExclamationCircleOutlined,
PlusOutlined,
} from '@ant-design/icons';
import {
Alert,
Button,
Checkbox,
Divider,
Dropdown,
List,
Modal,
Space,
Typography,
} from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { EntityDetailUnion } from 'Models';
import VirtualList from 'rc-virtual-list';
import {
@ -20,10 +38,11 @@ import {
UIEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { PAGE_SIZE_MEDIUM } from '../../../constants/constants';
import { ERROR_COLOR, PAGE_SIZE_MEDIUM } from '../../../constants/constants';
import { SearchIndex } from '../../../enums/search.enum';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
import { DataProduct } from '../../../generated/entity/domains/dataProduct';
@ -33,7 +52,11 @@ import {
Status,
} from '../../../generated/type/bulkOperationResult';
import { Aggregations } from '../../../interface/search.interface';
import { QueryFilterInterface } from '../../../pages/ExplorePage/ExplorePage.interface';
import {
QueryFieldInterface,
QueryFieldValueInterface,
QueryFilterInterface,
} from '../../../pages/ExplorePage/ExplorePage.interface';
import {
addAssetsToDataProduct,
getDataProductByName,
@ -53,11 +76,11 @@ import {
import { getCombinedQueryFilterObject } from '../../../utils/ExplorePage/ExplorePageUtils';
import { getEncodedFqn } from '../../../utils/StringsUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import AssetFilters from '../../AssetFilters/AssetFilters.component';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Searchbar from '../../common/SearchBarComponent/SearchBar.component';
import TableDataCardV2 from '../../common/TableDataCardV2/TableDataCardV2';
import { ExploreQuickFilterField } from '../../Explore/ExplorePage.interface';
import ExploreQuickFilters from '../../Explore/ExploreQuickFilters';
import { AssetsOfEntity } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
import Loader from '../../Loader/Loader';
import { SearchedDataProps } from '../../SearchedData/SearchedData.interface';
@ -103,6 +126,20 @@ export const AssetSelectionModal = ({
},
})
);
const [selectedFilter, setSelectedFilter] = useState<string[]>([]);
const [filters, setFilters] = useState<ExploreQuickFilterField[]>([]);
const handleMenuClick = ({ key }: { key: string }) => {
setSelectedFilter((prevSelected) => [...prevSelected, key]);
};
const filterMenu: ItemType[] = useMemo(() => {
return filters.map((filter) => ({
key: filter.key,
label: filter.label,
onClick: handleMenuClick,
}));
}, [filters]);
const fetchEntities = useCallback(
async ({
@ -153,10 +190,14 @@ export const AssetSelectionModal = ({
useEffect(() => {
const dropdownItems = getAssetsPageQuickFilters(type);
setSelectedQuickFilters(
setFilters(
dropdownItems.map((item) => ({
...item,
value: getSelectedValuesFromQuickFilter(item, dropdownItems),
value: getSelectedValuesFromQuickFilter(
item,
dropdownItems,
undefined // pass in state variable
),
}))
);
}, [type]);
@ -213,6 +254,7 @@ export const AssetSelectionModal = ({
const handleSave = async () => {
try {
setIsSaveLoading(true);
setFailedStatus(undefined);
if (!activeEntity) {
return;
}
@ -283,6 +325,29 @@ export const AssetSelectionModal = ({
mergeFilters();
}, [quickFilterQuery, queryFilter]);
useEffect(() => {
const updatedQuickFilters = filters
.filter((filter) => selectedFilter.includes(filter.key))
.map((selectedFilterItem) => {
const originalFilterItem = selectedQuickFilters?.find(
(filter) => filter.key === selectedFilterItem.key
);
return originalFilterItem || selectedFilterItem;
});
const newItems = updatedQuickFilters.filter(
(item) =>
!selectedQuickFilters.some(
(existingItem) => item.key === existingItem.key
)
);
if (newItems.length > 0) {
setSelectedQuickFilters((prevSelected) => [...prevSelected, ...newItems]);
}
}, [selectedFilter, selectedQuickFilters, filters]);
const onScroll: UIEventHandler<HTMLElement> = useCallback(
(e) => {
const scrollHeight =
@ -353,6 +418,66 @@ export const AssetSelectionModal = ({
[failedStatus]
);
const handleQuickFiltersChange = (data: ExploreQuickFilterField[]) => {
const must: QueryFieldInterface[] = [];
data.forEach((filter) => {
if (!isEmpty(filter.value)) {
const should: QueryFieldValueInterface[] = [];
if (filter.value) {
filter.value.forEach((filterValue) => {
const term: Record<string, string> = {};
term[filter.key] = filterValue.key;
should.push({ term });
});
}
must.push({
bool: { should },
});
}
});
const quickFilterQuery = isEmpty(must)
? undefined
: {
query: { bool: { must } },
};
setQuickFilterQuery(quickFilterQuery);
};
const handleQuickFiltersValueSelect = useCallback(
(field: ExploreQuickFilterField) => {
setSelectedQuickFilters((pre) => {
const data = pre.map((preField) => {
if (preField.key === field.key) {
return field;
} else {
return preField;
}
});
handleQuickFiltersChange(data);
return data;
});
},
[setSelectedQuickFilters]
);
const clearFilters = useCallback(() => {
setQuickFilterQuery(undefined);
setSelectedQuickFilters((pre) => {
const data = pre.map((preField) => {
return { ...preField, value: [] };
});
handleQuickFiltersChange(data);
return data;
});
}, [setQuickFilterQuery, handleQuickFiltersChange, setSelectedQuickFilters]);
return (
<Modal
destroyOnClose
@ -362,12 +487,23 @@ export const AssetSelectionModal = ({
data-testid="asset-selection-modal"
footer={
<div className="d-flex justify-between">
<div>
{selectedItems && selectedItems.size > 1 && (
<Typography.Text>
<div className="d-flex items-center gap-2">
{selectedItems && selectedItems.size >= 1 && (
<Typography.Text className="gap-2">
<CheckOutlined className="text-success m-r-xs" />
{selectedItems.size} {t('label.selected-lowercase')}
</Typography.Text>
)}
{failedStatus?.failedRequest &&
failedStatus.failedRequest.length > 0 && (
<>
<Divider className="m-x-xss" type="vertical" />
<Typography.Text type="danger">
<CloseOutlined className="m-r-xs" />
{failedStatus.failedRequest.length} {t('label.error')}
</Typography.Text>
</>
)}
</div>
<div>
@ -376,7 +512,7 @@ export const AssetSelectionModal = ({
</Button>
<Button
data-testid="save-btn"
disabled={isLoading}
disabled={!selectedItems?.size || isLoading}
loading={isSaveLoading}
type="primary"
onClick={onSaveAction}>
@ -391,6 +527,16 @@ export const AssetSelectionModal = ({
width={675}
onCancel={onCancel}>
<Space className="w-full h-full" direction="vertical" size={16}>
<div className="d-flex items-center gap-3">
<Dropdown
menu={{
items: filterMenu,
selectedKeys: selectedFilter,
}}
trigger={['click']}>
<Button icon={<PlusOutlined />} size="small" type="primary" />
</Dropdown>
<div className="flex-1">
<Searchbar
removeMargin
showClearSearch
@ -400,16 +546,54 @@ export const AssetSelectionModal = ({
searchValue={search}
onSearch={setSearch}
/>
<div className="d-flex items-center">
<AssetFilters
aggregations={aggregations}
filterData={selectedQuickFilters}
quickFilterQuery={quickFilterQuery}
type={type}
onQuickFilterChange={(data) => setQuickFilterQuery(data)}
/>
</div>
</div>
{selectedQuickFilters && selectedQuickFilters.length > 0 && (
<div className="d-flex items-center">
<div className="d-flex justify-between flex-1">
<ExploreQuickFilters
aggregations={aggregations}
fields={selectedQuickFilters}
index={SearchIndex.ALL}
showDeleted={false}
onFieldValueSelect={handleQuickFiltersValueSelect}
/>
{quickFilterQuery && (
<Typography.Text
className="p-r-xss text-primary self-center cursor-pointer"
onClick={clearFilters}>
{t('label.clear-entity', {
entity: '',
})}
</Typography.Text>
)}
</div>
</div>
)}
{failedStatus?.failedRequest && failedStatus.failedRequest.length > 0 && (
<Alert
closable
className="w-full"
description={
<Typography.Text className="text-grey-muted">
{t('message.validation-error-assets')}
</Typography.Text>
}
message={
<div className="d-flex items-center gap-3">
<ExclamationCircleOutlined
style={{ color: ERROR_COLOR, fontSize: '24px' }}
/>
<Typography.Text className="font-semibold text-sm">
{t('label.validation-error-plural')}
</Typography.Text>
</div>
}
type="error"
/>
)}
{items.length > 0 && (
<div className="border p-xs">
@ -435,7 +619,8 @@ export const AssetSelectionModal = ({
<div
className={classNames({
'm-y-sm border-danger rounded-4': isError,
})}>
})}
key={item.id}>
<TableDataCardV2
openEntityInNewPage
showCheckboxes
@ -449,11 +634,19 @@ export const AssetSelectionModal = ({
source={{ ...item, tags: [] }}
/>
{isError && (
<div className="p-x-sm p-b-sm">
<Typography.Text type="danger">
<>
<div className="p-x-sm">
<Divider className="m-t-0 m-y-sm " />
</div>
<div className="d-flex gap-3 p-x-sm p-b-sm">
<ExclamationCircleOutlined
style={{ color: ERROR_COLOR, fontSize: '24px' }}
/>
<Typography.Text className="break-all">
{errorMessage}
</Typography.Text>
</div>
</>
)}
</div>
);

View File

@ -14,7 +14,7 @@
@import url('../../../styles/variables.less');
.asset-selection-model-card.table-data-card-container {
box-shadow: none;
box-shadow: none !important;
}
.asset-selection-model-card.table-data-card-container:hover {
box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.13);

View File

@ -14,6 +14,7 @@ import { Col, Row, Space, Tag, Typography } from 'antd';
import { isEmpty } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { ReactComponent as EditIcon } from '../../assets/svg/edit-new.svg';
import { ReactComponent as DataProductIcon } from '../../assets/svg/ic-data-product.svg';
import DataProductSelectForm from '../../components/DataProductSelectForm/DataProductsSelectForm';
@ -27,6 +28,8 @@ import { DataProduct } from '../../generated/entity/domains/dataProduct';
import { EntityReference } from '../../generated/entity/type';
import { fetchDataProductsElasticSearch } from '../../rest/dataProductAPI';
import { getEntityName } from '../../utils/EntityUtils';
import { getDataProductsDetailsPath } from '../../utils/RouterUtils';
import { getEncodedFqn } from '../../utils/StringsUtils';
interface DataProductsContainerProps {
showHeader?: boolean;
@ -44,6 +47,7 @@ const DataProductsContainer = ({
onSave,
}: DataProductsContainerProps) => {
const { t } = useTranslation();
const history = useHistory();
const [isEditMode, setIsEditMode] = useState(false);
const handleAddClick = () => {
@ -70,6 +74,10 @@ const DataProductsContainer = ({
[activeDomain]
);
const redirectLink = useCallback((fqn) => {
history.push(getDataProductsDetailsPath(getEncodedFqn(fqn)));
}, []);
const handleSave = async (dataProducts: DataProduct[]) => {
await onSave?.(dataProducts);
setIsEditMode(false);
@ -107,7 +115,8 @@ const DataProductsContainer = ({
return (
<Tag
className="tag-chip tag-chip-content"
key={`dp-tags-${product.fullyQualifiedName}`}>
key={`dp-tags-${product.fullyQualifiedName}`}
onClick={() => redirectLink(product.fullyQualifiedName)}>
<div className="d-flex w-full">
<div className="d-flex items-center p-x-xs w-full gap-1">
<DataProductIcon

View File

@ -83,6 +83,7 @@ import {
} from '../../../utils/RouterUtils';
import {
escapeESReservedCharacters,
getDecodedFqn,
getEncodedFqn,
} from '../../../utils/StringsUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
@ -111,7 +112,7 @@ const DomainDetailsPage = ({
tab: activeTab,
version,
} = useParams<{ fqn: string; tab: string; version: string }>();
const domainFqn = fqn ? decodeURIComponent(fqn) : '';
const domainFqn = fqn ? getDecodedFqn(fqn) : '';
const assetTabRef = useRef<AssetsTabRef>(null);
const dataProductsTabRef = useRef<DataProductsTabRef>(null);
const [domainPermission, setDomainPermission] = useState<OperationPermission>(
@ -512,7 +513,7 @@ const DomainDetailsPage = ({
fetchDomainPermission();
fetchDomainAssets();
fetchDataProducts();
}, [fqn]);
}, [domain.fullyQualifiedName]);
return (
<>

View File

@ -173,11 +173,15 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
<Row gutter={[8, 8]}>
{showCheckboxes && (
<Col flex="25px">
<div onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={checked}
className="assets-checkbox"
onChange={(e) => onCheckboxChange?.(e.target.checked)}
onChange={(e) => {
onCheckboxChange?.(e.target.checked);
}}
/>
</div>
</Col>
)}
{!hideBreadcrumbs && (

View File

@ -36,7 +36,10 @@ import { getCountBadge, getFeedCounts } from '../../../utils/CommonUtils';
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
import { getQueryFilterToExcludeTerm } from '../../../utils/GlossaryUtils';
import { getGlossaryTermsVersionsPath } from '../../../utils/RouterUtils';
import { escapeESReservedCharacters } from '../../../utils/StringsUtils';
import {
escapeESReservedCharacters,
getEncodedFqn,
} from '../../../utils/StringsUtils';
import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
import { AssetSelectionModal } from '../../Assets/AssetsSelectionModal/AssetSelectionModal';
import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable';
@ -258,13 +261,14 @@ const GlossaryTermsV1 = ({
const fetchGlossaryTermAssets = async () => {
if (glossaryTerm) {
try {
const encodedFqn = getEncodedFqn(
escapeESReservedCharacters(glossaryTerm.fullyQualifiedName)
);
const res = await searchData(
'',
1,
0,
`(tags.tagFQN:"${escapeESReservedCharacters(
glossaryTerm.fullyQualifiedName
)}")`,
`(tags.tagFQN:"${encodedFqn}")`,
'',
'',
SearchIndex.ALL

View File

@ -14,11 +14,11 @@
import { PlusOutlined } from '@ant-design/icons';
import {
Affix,
Button,
Checkbox,
Col,
Dropdown,
Menu,
MenuProps,
notification,
Row,
@ -136,6 +136,7 @@ const AssetsTabs = forwardRef(
const [itemCount, setItemCount] = useState<Record<EntityType, number>>(
{} as Record<EntityType, number>
);
const [assetRemoving, setAssetRemoving] = useState(false);
const [activeFilter, _] = useState<SearchIndex[]>([]);
const { fqn } = useParams<{ fqn: string }>();
@ -173,8 +174,9 @@ const AssetsTabs = forwardRef(
Domain | DataProduct | GlossaryTerm
>();
const [selectedItems, setSelectedItems] =
useState<Map<string, EntityDetailUnion>>();
const [selectedItems, setSelectedItems] = useState<
Map<string, EntityDetailUnion>
>(new Map());
const [aggregations, setAggregations] = useState<Aggregations>();
const [selectedFilter, setSelectedFilter] = useState<string[]>([]); // Contains menu selection
const [selectedQuickFilters, setSelectedQuickFilters] = useState<
@ -193,16 +195,13 @@ const AssetsTabs = forwardRef(
setSelectedFilter((prevSelected) => [...prevSelected, key]);
};
const filterMenu = useMemo(
() => (
<Menu selectedKeys={selectedFilter} onClick={handleMenuClick}>
{filters.map((filter) => (
<Menu.Item key={filter.key}>{filter.label}</Menu.Item>
))}
</Menu>
),
[selectedFilter, filters]
);
const filterMenu: ItemType[] = useMemo(() => {
return filters.map((filter) => ({
key: filter.key,
label: filter.label,
onClick: handleMenuClick,
}));
}, [filters]);
const queryParam = useMemo(() => {
const encodedFqn = getEncodedFqn(escapeESReservedCharacters(entityFqn));
@ -425,6 +424,7 @@ const AssetsTabs = forwardRef(
return () => {
onAssetClick?.(undefined);
hideNotification();
};
}, []);
@ -434,15 +434,6 @@ const AssetsTabs = forwardRef(
}
}, [entityFqn]);
useEffect(() => {
if (selectedItems) {
hideNotification();
if (selectedItems.size > 1) {
openNotification();
}
}
}, [selectedItems]);
const assetErrorPlaceHolder = useMemo(() => {
if (!isEmpty(activeFilter)) {
return (
@ -713,6 +704,8 @@ const AssetsTabs = forwardRef(
return;
}
setAssetRemoving(true);
try {
const entities = [...(assetsData?.values() ?? [])].map((item) => {
return getEntityReferenceFromEntity(
@ -760,6 +753,7 @@ const AssetsTabs = forwardRef(
} finally {
setShowDeleteModal(false);
onRemoveAsset?.();
setAssetRemoving(false);
hideNotification();
setSelectedItems(new Map()); // Reset selected items
}
@ -784,31 +778,6 @@ const AssetsTabs = forwardRef(
setSelectedQuickFilters,
]);
const openNotification = () => {
notification.warning({
key: 'asset-tab-notification-key',
message: (
<div className="d-flex items-center justify-between">
{selectedItems && selectedItems.size > 1 && (
<Typography.Text className="text-white">
{selectedItems.size} {t('label.items-selected-lowercase')}
</Typography.Text>
)}
<Button
danger
data-testid="delete-all-button"
type="primary"
onClick={deleteSelectedItems}>
{t('label.delete')}
</Button>
</div>
),
placement: 'bottom',
className: 'asset-tab-delete-notification',
duration: 0,
});
};
useEffect(() => {
fetchAssets({
index: isEmpty(activeFilter) ? [SearchIndex.ALL] : activeFilter,
@ -861,7 +830,7 @@ const AssetsTabs = forwardRef(
refreshAssets() {
fetchAssets({
index: isEmpty(activeFilter) ? [SearchIndex.ALL] : activeFilter,
page: currentPage,
page: 1,
});
fetchCountsByEntity();
},
@ -885,12 +854,18 @@ const AssetsTabs = forwardRef(
return (
<div
className={classNames('assets-tab-container p-md')}
data-testid="table-container">
data-testid="table-container"
id="asset-tab">
{assetCount > 0 && (
<Row className="filters-row gap-2 p-l-lg">
<Col span={18}>
<div className="d-flex items-center gap-3">
<Dropdown overlay={filterMenu} trigger={['click']}>
<Dropdown
menu={{
items: filterMenu,
selectedKeys: selectedFilter,
}}
trigger={['click']}>
<Button icon={<PlusOutlined />} size="small" type="primary" />
</Dropdown>
<div className="flex-1">
@ -952,10 +927,36 @@ const AssetsTabs = forwardRef(
header={t('label.remove-entity', {
entity: getEntityName(assetToDelete) + '?',
})}
isLoading={assetRemoving}
visible={showDeleteModal}
onCancel={() => setShowDeleteModal(false)}
onConfirm={() => onAssetRemove(assetToDelete ? [assetToDelete] : [])}
/>
{selectedItems.size > 1 && (
<Affix
className={classNames('asset-tab-delete-notification', {
visible: selectedItems.size > 1,
})}
offsetBottom={20}
target={() =>
document.getElementById('asset-tab') || document.body
}>
<div className="d-flex items-center justify-between">
<Typography.Text className="text-white">
{selectedItems.size} {t('label.items-selected-lowercase')}
</Typography.Text>
<Button
danger
data-testid="delete-all-button"
loading={assetRemoving}
type="primary"
onClick={deleteSelectedItems}>
{t('label.delete')}
</Button>
</div>
</Affix>
)}
</div>
);
}

View File

@ -83,10 +83,6 @@ const GlossaryOverviewTab = ({
}
}, [selectedData, isVersionView]);
const handleTagsUpdate = async (updatedTags: TagLabel[]) => {
setTagsUpdating(updatedTags);
};
const tags = useMemo(
() =>
isVersionView
@ -98,14 +94,16 @@ const GlossaryOverviewTab = ({
[isVersionView, selectedData]
);
const handleTagsUpdate = async (updatedTags: TagLabel[]) => {
setTagsUpdating(updatedTags);
};
const handleGlossaryTagUpdateValidationConfirm = async () => {
if (selectedData) {
await onUpdate({
...selectedData,
tags: tagsUpdatating,
});
setTagsUpdating(undefined);
}
};

View File

@ -81,20 +81,24 @@
}
.asset-tab-delete-notification {
background-color: @text-color !important;
box-shadow: none !important;
.ant-notification-notice-icon {
display: none;
}
.ant-notification-notice-close {
color: @white !important;
left: 24px;
top: 22px;
width: 20px;
}
.ant-notification-notice-message {
padding-right: 0 !important;
margin-left: 20px !important;
font-size: 14px !important;
&.visible {
.ant-affix {
bottom: 10px !important;
}
}
.ant-affix {
border-radius: 10px;
background: #292929;
height: 64px !important;
width: 350px !important;
bottom: -64px !important;
left: 40%;
transform: translateX(-60%);
transition: bottom ease-in 0.5s;
& > div {
padding: 15px 20px;
}
}
}

View File

@ -19,3 +19,11 @@ export interface GlossaryUpdateConfirmationModalProps {
onCancel: () => void;
updatedTags: TagLabel[];
}
export enum UpdateState {
INITIAL,
VALIDATING,
FAILED,
UPDATATING,
SUCESS,
}

View File

@ -11,8 +11,7 @@
* limitations under the License.
*/
import Icon from '@ant-design/icons';
import { Button, Modal, Space, Typography } from 'antd';
import { isEmpty } from 'lodash';
import { Alert, Button, Modal, Progress, Space, Typography } from 'antd';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@ -31,7 +30,10 @@ import {
getEntityName,
} from '../../../utils/EntityUtils';
import Table from '../../common/Table/Table';
import { GlossaryUpdateConfirmationModalProps } from './GlossaryUpdateConfirmationModal.interface';
import {
GlossaryUpdateConfirmationModalProps,
UpdateState,
} from './GlossaryUpdateConfirmationModal.interface';
export const GlossaryUpdateConfirmationModal = ({
glossaryTerm,
@ -41,13 +43,12 @@ export const GlossaryUpdateConfirmationModal = ({
}: GlossaryUpdateConfirmationModalProps) => {
const [failedStatus, setFailedStatus] = useState<BulkOperationResult>();
const [tagError, setTagError] = useState<{ code: number; message: string }>();
const [tagAdditionConfirmation, setTagAdditionConfirmation] = useState(false);
const [validating, setValidating] = useState(false);
const [updateState, setUpdateState] = useState(UpdateState.INITIAL);
const { t } = useTranslation();
const handleUpdateConfirmation = async () => {
setTagAdditionConfirmation(true);
setValidating(true);
setUpdateState(UpdateState.VALIDATING);
try {
// dryRun validations so that we can list failures if any
const res = await validateTagAddtionToGlossary(
@ -55,16 +56,24 @@ export const GlossaryUpdateConfirmationModal = ({
true
);
if (res.status && res.status === Status.Success) {
if (res.status === Status.Success) {
setUpdateState(UpdateState.UPDATATING);
try {
await onValidationSuccess();
setUpdateState(UpdateState.SUCESS);
} catch (err) {
// Error
} finally {
setTimeout(onCancel, 500);
}
} else {
setUpdateState(UpdateState.FAILED);
setFailedStatus(res);
}
} catch (err) {
// error
setTagError(err.response?.data);
} finally {
setValidating(false);
setUpdateState(UpdateState.FAILED);
}
};
@ -96,14 +105,15 @@ export const GlossaryUpdateConfirmationModal = ({
];
}, []);
return (
<Modal
centered
open
closable={false}
closeIcon={null}
footer={
tagAdditionConfirmation && (
const progress =
updateState === UpdateState.VALIDATING
? 10
: updateState === UpdateState.UPDATATING
? 60
: 100;
const data = useMemo(() => {
const footer = (
<div className="d-flex justify-between">
<Typography.Text type="secondary">
{failedStatus?.numberOfRowsFailed &&
@ -111,40 +121,19 @@ export const GlossaryUpdateConfirmationModal = ({
</Typography.Text>
<Button onClick={onCancel}>{t('label.cancel')}</Button>
</div>
)
}
title={
tagAdditionConfirmation
? t('message.glossary-tag-update-modal-title')
: undefined
}
width={tagAdditionConfirmation ? 750 : undefined}
onCancel={onCancel}>
{tagAdditionConfirmation || validating ? (
<div className="d-flex flex-column gap-2">
{!isEmpty(failedStatus?.failedRequest) && !validating && (
<>
<Table
bordered
columns={tagsColumn}
dataSource={failedStatus?.failedRequest}
loading={validating}
pagination={{
pageSize: 5,
showSizeChanger: true,
}}
rowKey={(record) => record.request.id}
/>
<Typography.Text italic className="m-t-sm" type="secondary">
{t('message.glossary-tag-assignement-help-message')}
</Typography.Text>
</>
)}
{tagError?.code === ClientErrors.BAD_REQUEST && (
<Typography.Text type="danger">{tagError.message}</Typography.Text>
)}
);
const progressBar = (
<div className="text-center">
<Progress percent={progress} status="normal" type="circle" />
</div>
) : (
);
switch (updateState) {
case UpdateState.INITIAL:
return {
footer: null,
content: (
<div className="d-flex items-center flex-column gap-2">
<Icon
className="m-b-lg"
@ -156,21 +145,99 @@ export const GlossaryUpdateConfirmationModal = ({
</Typography.Title>
<Typography.Text className="text-center">
{t('message.glossary-tag-update-description')}{' '}
<span className="font-medium">{getEntityName(glossaryTerm)}</span>
<span className="font-medium">
{getEntityName(glossaryTerm)}
</span>
</Typography.Text>
<div className="m-t-lg">
<Space size={8}>
<Button onClick={onCancel}>{t('label.no-comma-cancel')}</Button>
<Button
loading={validating}
type="primary"
onClick={handleUpdateConfirmation}>
<Button onClick={onCancel}>
{t('label.no-comma-cancel')}
</Button>
<Button type="primary" onClick={handleUpdateConfirmation}>
{t('label.yes-comma-confirm')}
</Button>
</Space>
</div>
</div>
),
};
case UpdateState.VALIDATING:
return {
content: progressBar,
footer: footer,
};
case UpdateState.FAILED:
return {
content: (
<div className="d-flex flex-column gap-2">
{failedStatus && (
<>
<Table
bordered
columns={tagsColumn}
dataSource={failedStatus?.failedRequest ?? []}
pagination={{
pageSize: 5,
showSizeChanger: true,
}}
rowKey={(record) => record.request?.id}
/>
<Alert
className="m-t-sm"
message={t('message.glossary-tag-assignement-help-message')}
type="warning"
/>
</>
)}
{tagError?.code === ClientErrors.BAD_REQUEST && (
<Alert message={tagError.message} type="warning" />
)}
</div>
),
footer: (
<div className="d-flex justify-between">
<Typography.Text type="secondary">
{failedStatus?.numberOfRowsFailed &&
`${failedStatus.numberOfRowsFailed} ${t('label.failed')}`}
</Typography.Text>
<Button onClick={onCancel}>{t('label.cancel')}</Button>
</div>
),
};
case UpdateState.UPDATATING:
return {
content: progressBar,
footer: <Button onClick={onCancel}>{t('label.cancel')}</Button>,
};
case UpdateState.SUCESS:
return {
content: progressBar,
footer: <Button onClick={onCancel}>{t('label.cancel')}</Button>,
};
}
}, [updateState, failedStatus]);
return (
<Modal
centered
open
closable={false}
closeIcon={null}
footer={data.footer}
title={
[
UpdateState.VALIDATING,
UpdateState.FAILED,
UpdateState.UPDATATING,
UpdateState.SUCESS,
].includes(updateState)
? t('message.glossary-tag-update-modal-title')
: undefined
}
width={updateState === UpdateState.FAILED ? 750 : undefined}
onCancel={onCancel}>
{data.content}
</Modal>
);
};

View File

@ -13,7 +13,7 @@
import { Col, Form, Row, Space, Tooltip, Typography } from 'antd';
import { DefaultOptionType } from 'antd/lib/select';
import { isEmpty } from 'lodash';
import { isEmpty, isEqual } from 'lodash';
import { EntityTags } from 'Models';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -104,7 +104,7 @@ const TagsContainerV2 = ({
const handleSave = async (data: DefaultOptionType | DefaultOptionType[]) => {
const updatedTags = (data as DefaultOptionType[]).map((tag) => {
let tagData: EntityTags = {
tagFQN: tag.value,
tagFQN: typeof tag === 'string' ? tag : tag.value,
source: tagType,
};
@ -115,13 +115,16 @@ const TagsContainerV2 = ({
displayName: tag.data?.displayName,
description: tag.data?.description,
style: tag.data?.style,
labelType: tag.data?.labelType,
};
}
return tagData;
});
if (onSelectionChange) {
const newTags = updatedTags.map((t) => t.tagFQN);
if (onSelectionChange && !isEqual(selectedTagsInternal, newTags)) {
await onSelectionChange([
...updatedTags,
...((isGlossaryType

View File

@ -30,6 +30,7 @@ import {
export const PRIMERY_COLOR = '#0968da';
export const SECONDARY_COLOR = '#B02AAC';
export const INFO_COLOR = '#2196f3';
export const ERROR_COLOR = '#ff4c3b';
export const LITE_GRAY_COLOR = '#DBE0EB';
export const TEXT_BODY_COLOR = '#37352F';
export const TEXT_GREY_MUTED = '#757575';

View File

@ -396,6 +396,7 @@
"entity-service": "{{entity}}-Dienst",
"entity-type-plural": "{{entity}}-Typen",
"entity-version-detail-plural": "Details zu {{entity}}-Versionen",
"error": "Error",
"event-publisher-plural": "Ereignisveröffentlicher",
"event-type": "Ereignistyp",
"every": "Jede/r/s",
@ -1133,6 +1134,7 @@
"username-or-email": "Benutzername oder E-Mail",
"valid-condition": "Gültige Bedingung",
"validating-condition": "Bedingung validieren...",
"validation-error-plural": "Validation Errors!",
"value": "Wert",
"value-count": "Wertanzahl",
"value-plural": "Werte",
@ -1628,6 +1630,7 @@
"user-mentioned-in-comment": "{{user}} hat Sie in einem Kommentar erwähnt.",
"user-verified-successfully": "Benutzer erfolgreich verifiziert",
"valid-url-endpoint": "Endpunkte sollten gültige URLs sein",
"validation-error-assets": "Please examine all your assets that are being added",
"version-released-try-now": "{{version}} Veröffentlicht <0>Sehen Sie, was es Neues gibt!</0>",
"view-deleted-entity": "Alle gelöschten {{entity}}, die zu diesem {{parent}} gehören, anzeigen.",
"view-sample-data-entity": "Um Musterdaten anzuzeigen, führen Sie {{entity}} aus. Bitte beachten Sie dieses Dokument, um die <0>{{entity}}</0> zu planen.",

View File

@ -396,6 +396,7 @@
"entity-service": "{{entity}} Service",
"entity-type-plural": "{{entity}} Type",
"entity-version-detail-plural": "{{entity}} Version Details",
"error": "Error",
"event-publisher-plural": "Event Publishers",
"event-type": "Event Type",
"every": "Every",
@ -1133,6 +1134,7 @@
"username-or-email": "Username or Email",
"valid-condition": "Valid condition",
"validating-condition": "Validating the condition...",
"validation-error-plural": "Validation Errors!",
"value": "Value",
"value-count": "Value Count",
"value-plural": "Values",
@ -1628,6 +1630,7 @@
"user-mentioned-in-comment": "{{user}} mentioned you in a comment.",
"user-verified-successfully": "User Verified Successfully",
"valid-url-endpoint": "Endpoints should be valid URL",
"validation-error-assets": "Please examine all your assets that are being added",
"version-released-try-now": "{{version}} Released <0>See What's New!</0>",
"view-deleted-entity": "View All the Deleted {{entity}}, which come under this {{parent}}.",
"view-sample-data-entity": "To view Sample Data, run the {{entity}}. Please refer to this doc to schedule the <0>{{entity}}</0>",

View File

@ -396,6 +396,7 @@
"entity-service": "Servicio de {{entity}}",
"entity-type-plural": "{{entity}} Type",
"entity-version-detail-plural": "{{entity}} Version Details",
"error": "Error",
"event-publisher-plural": "Publicadores de eventos",
"event-type": "Tipo de evento",
"every": "Cada",
@ -1133,6 +1134,7 @@
"username-or-email": "Nombre de usuario o correo electrónico",
"valid-condition": "Condición válida",
"validating-condition": "Validando la condición...",
"validation-error-plural": "Validation Errors!",
"value": "Valor",
"value-count": "Recuento de valor",
"value-plural": "Valores",
@ -1628,6 +1630,7 @@
"user-mentioned-in-comment": "{{user}} te mencionó en un comentario.",
"user-verified-successfully": "Usuario verificado con éxito",
"valid-url-endpoint": "Los terminales deben ser URL válidas",
"validation-error-assets": "Please examine all your assets that are being added",
"version-released-try-now": "{{version}} Released <0>See What's New!</0>",
"view-deleted-entity": "Ver todas las {{entity}} eliminadas que se encuentran bajo este {{parent}}.",
"view-sample-data-entity": "To view Sample Data, run the {{entity}}. Please refer to this doc to schedule the <0>{{entity}}</0>",

View File

@ -396,6 +396,7 @@
"entity-service": "Service de {{entity}}",
"entity-type-plural": "{{entity}} Types",
"entity-version-detail-plural": "Détails des Versions de {{entity}}",
"error": "Error",
"event-publisher-plural": "Publicateurs d'Événements",
"event-type": "Type d'Événement",
"every": "Chaque",
@ -1133,6 +1134,7 @@
"username-or-email": "Nom d'Utilisateur ou Email",
"valid-condition": "Condition Valide",
"validating-condition": "Validation de la Condition...",
"validation-error-plural": "Validation Errors!",
"value": "Valeur",
"value-count": "Décompte de la Valeur",
"value-plural": "Valeurs",
@ -1628,6 +1630,7 @@
"user-mentioned-in-comment": "{{user}} vous a mentionné dans un commentaire.",
"user-verified-successfully": "L'utilisateur a été vérifié avec succès",
"valid-url-endpoint": "Les points de terminaison doivent être une URL valide",
"validation-error-assets": "Please examine all your assets that are being added",
"version-released-try-now": "{{version}} publié essayez maintenant !",
"view-deleted-entity": "Afficher toutes les {{entity}} supprimées qui relèvent de ce {{parent}}.",
"view-sample-data-entity": "Pour afficher des données d'exemple, exécutez {{entity}}. Veuillez vous référer à ce document pour planifier {{entity}}.",

View File

@ -396,6 +396,7 @@
"entity-service": "{{entity}}サービス",
"entity-type-plural": "{{entity}} Type",
"entity-version-detail-plural": "{{entity}} Version Details",
"error": "Error",
"event-publisher-plural": "イベントの作成者",
"event-type": "イベントの種類",
"every": "Every",
@ -1133,6 +1134,7 @@
"username-or-email": "ユーザ名あるいはEmailアドレス",
"valid-condition": "正しい条件",
"validating-condition": "条件を検証中...",
"validation-error-plural": "Validation Errors!",
"value": "値",
"value-count": "値の数",
"value-plural": "値",
@ -1628,6 +1630,7 @@
"user-mentioned-in-comment": "{{user}}がコメントであなたにメンションしました。",
"user-verified-successfully": "ユーザ認証に成功しました",
"valid-url-endpoint": "エンドポイントは有効なURLでなければなりません",
"validation-error-assets": "Please examine all your assets that are being added",
"version-released-try-now": "{{version}} Released <0>See What's New!</0>",
"view-deleted-entity": "View All the Deleted {{entity}}, which come under this {{parent}}.",
"view-sample-data-entity": "To view Sample Data, run the {{entity}}. Please refer to this doc to schedule the <0>{{entity}}</0>",

View File

@ -396,6 +396,7 @@
"entity-service": "Serviço de {{entity}}",
"entity-type-plural": "Tipo de {{entity}}",
"entity-version-detail-plural": "Detalhes da Versão de {{entity}}",
"error": "Error",
"event-publisher-plural": "Publicadores de Eventos",
"event-type": "Tipo de Evento",
"every": "Cada",
@ -1133,6 +1134,7 @@
"username-or-email": "Nome de Usuário ou Email",
"valid-condition": "Condição Válida",
"validating-condition": "Validando a condição...",
"validation-error-plural": "Validation Errors!",
"value": "Valor",
"value-count": "Contagem de Valor",
"value-plural": "Valores",
@ -1628,6 +1630,7 @@
"user-mentioned-in-comment": "{{user}} mencionou você em um comentário.",
"user-verified-successfully": "Usuário verificado com sucesso",
"valid-url-endpoint": "Os endpoints devem ser URLs válidas",
"validation-error-assets": "Please examine all your assets that are being added",
"version-released-try-now": "{{version}} Lançada <0>Veja o que há de novo!</0>",
"view-deleted-entity": "Visualize todos os {{entity}} excluídos, que fazem parte deste {{parent}}.",
"view-sample-data-entity": "Para visualizar os Dados de Amostra, execute o {{entity}}. Consulte este documento para agendar o <0>{{entity}}</0>",

View File

@ -396,6 +396,7 @@
"entity-service": "Сервис {{entity}}",
"entity-type-plural": "Тип {{entity}}",
"entity-version-detail-plural": "{{entity}} Version Details",
"error": "Error",
"event-publisher-plural": "Издатели события",
"event-type": "Тип события",
"every": "Каждый",
@ -1133,6 +1134,7 @@
"username-or-email": "Имя пользователя или Email",
"valid-condition": "Действительное условие",
"validating-condition": "Проверка условия...",
"validation-error-plural": "Validation Errors!",
"value": "Значение",
"value-count": "Количество значений",
"value-plural": "Значения",
@ -1628,6 +1630,7 @@
"user-mentioned-in-comment": "{{user}} упомянул вас в комментарии.",
"user-verified-successfully": "Пользователь успешно проверен",
"valid-url-endpoint": "Конечные точки должны быть действительным URL",
"validation-error-assets": "Please examine all your assets that are being added",
"version-released-try-now": "Выпущена {{версия}} <0>Посмотрите, что нового!</0>",
"view-deleted-entity": "Просмотреть все удаленные {{entity}}, относящиеся к этому {{parent}}.",
"view-sample-data-entity": "Чтобы просмотреть образцы данных, запустите {{entity}}. Пожалуйста, обратитесь к этому документу, чтобы запланировать <0>{{entity}}</0>",

View File

@ -396,6 +396,7 @@
"entity-service": "{{entity}}服务",
"entity-type-plural": "{{entity}}类型",
"entity-version-detail-plural": "{{entity}}版本详情",
"error": "Error",
"event-publisher-plural": "事件发布者",
"event-type": "事件类型",
"every": "每个",
@ -1133,6 +1134,7 @@
"username-or-email": "用户名或电子邮箱",
"valid-condition": "有效条件",
"validating-condition": "正在验证条件...",
"validation-error-plural": "Validation Errors!",
"value": "值",
"value-count": "值数量",
"value-plural": "值",
@ -1628,6 +1630,7 @@
"user-mentioned-in-comment": "{{user}}在评论中提到了您",
"user-verified-successfully": "用户验证成功",
"valid-url-endpoint": "端点应该是有效的 URL",
"validation-error-assets": "Please examine all your assets that are being added",
"version-released-try-now": "查看新版本 {{version}} 的新特性!",
"view-deleted-entity": "查看所有被删除的{{entity}},隶属于该{{parent}}",
"view-sample-data-entity": "要查看样本数据,请运行{{entity}}。您还可以前往查看任务调度相关的信息<0>{{entity}}</0>。",

View File

@ -305,9 +305,12 @@ const GlossaryPage = () => {
.finally(() => setDeleteStatus(LOADING_STATE.INITIAL));
};
const handleAssetClick = (asset?: EntityDetailsObjectInterface) => {
const handleAssetClick = useCallback(
(asset?: EntityDetailsObjectInterface) => {
setPreviewAsset(asset);
};
},
[]
);
if (isLoading) {
return <Loader />;

View File

@ -15,15 +15,20 @@ import { Status } from '../generated/entity/applications/appRunRecord';
import { PipelineState } from '../generated/entity/services/ingestionPipelines/ingestionPipeline';
export const getStatusTypeForApplication = (status: Status) => {
if (status === Status.Failed) {
switch (status) {
case Status.Failed:
return StatusType.Failure;
} else if (status === Status.Success) {
return StatusType.Success;
} else if (status === Status.Running) {
return StatusType.Warning;
}
case Status.Success:
case Status.Completed:
return StatusType.Success;
case Status.Running:
return StatusType.Warning;
default:
return StatusType.Failure;
}
};
export const getStatusFromPipelineState = (status: PipelineState) => {