mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 10:26:09 +00:00
1013 lines
27 KiB
TypeScript
1013 lines
27 KiB
TypeScript
/*
|
|
* 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { Popover, Space, Tag, Typography } from 'antd';
|
|
import { ColumnsType } from 'antd/lib/table';
|
|
import { AxiosError } from 'axios';
|
|
import classNames from 'classnames';
|
|
import i18n from 'i18next';
|
|
import {
|
|
capitalize,
|
|
differenceWith,
|
|
isEmpty,
|
|
isEqual,
|
|
isNil,
|
|
isNull,
|
|
isUndefined,
|
|
uniqueId,
|
|
} from 'lodash';
|
|
import {
|
|
CurrentState,
|
|
EntityFieldThreadCount,
|
|
ExtraInfo,
|
|
FormattedTableData,
|
|
RecentlySearched,
|
|
RecentlySearchedData,
|
|
RecentlyViewed,
|
|
RecentlyViewedData,
|
|
} from 'Models';
|
|
import React, { FormEvent } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { reactLocalStorage } from 'reactjs-localstorage';
|
|
import AppState from '../AppState';
|
|
import { getFeedCount } from '../axiosAPIs/feedsAPI';
|
|
import { Button } from '../components/buttons/Button/Button';
|
|
import {
|
|
getDayCron,
|
|
getHourCron,
|
|
} from '../components/common/CronEditor/CronEditor.constant';
|
|
import ErrorPlaceHolder from '../components/common/error-with-placeholder/ErrorPlaceHolder';
|
|
import Loader from '../components/Loader/Loader';
|
|
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
|
|
import {
|
|
getTeamAndUserDetailsPath,
|
|
getUserPath,
|
|
imageTypes,
|
|
LOCALSTORAGE_RECENTLY_SEARCHED,
|
|
LOCALSTORAGE_RECENTLY_VIEWED,
|
|
TITLE_FOR_NON_OWNER_ACTION,
|
|
} from '../constants/constants';
|
|
import {
|
|
UrlEntityCharRegEx,
|
|
validEmailRegEx,
|
|
} from '../constants/regex.constants';
|
|
import { SIZE } from '../enums/common.enum';
|
|
import { EntityType, FqnPart, TabSpecificField } from '../enums/entity.enum';
|
|
import { Ownership } from '../enums/mydata.enum';
|
|
import { Bot } from '../generated/entity/bot';
|
|
import { Dashboard } from '../generated/entity/data/dashboard';
|
|
import { Database } from '../generated/entity/data/database';
|
|
import { GlossaryTerm } from '../generated/entity/data/glossaryTerm';
|
|
import { Pipeline } from '../generated/entity/data/pipeline';
|
|
import { Table } from '../generated/entity/data/table';
|
|
import { Topic } from '../generated/entity/data/topic';
|
|
import { Webhook } from '../generated/entity/events/webhook';
|
|
import { ThreadTaskStatus, ThreadType } from '../generated/entity/feed/thread';
|
|
import { Policy } from '../generated/entity/policies/policy';
|
|
import { PipelineType } from '../generated/entity/services/ingestionPipelines/ingestionPipeline';
|
|
import { Role } from '../generated/entity/teams/role';
|
|
import { Team } from '../generated/entity/teams/team';
|
|
import { EntityReference, User } from '../generated/entity/teams/user';
|
|
import { Paging } from '../generated/type/paging';
|
|
import { ServicesType } from '../interface/service.interface';
|
|
import jsonData from '../jsons/en';
|
|
import { getEntityFeedLink, getTitleCase } from './EntityUtils';
|
|
import Fqn from './Fqn';
|
|
import { LIST_CAP } from './PermissionsUtils';
|
|
import { getRoleWithFqnPath, getTeamsWithFqnPath } from './RouterUtils';
|
|
import { serviceTypeLogo } from './ServiceUtils';
|
|
import SVGIcons, { Icons } from './SvgUtils';
|
|
import { getTierFromSearchTableTags } from './TableUtils';
|
|
import { TASK_ENTITIES } from './TasksUtils';
|
|
import { showErrorToast } from './ToastUtils';
|
|
|
|
export const arraySorterByKey = (
|
|
key: string,
|
|
sortDescending = false
|
|
): Function => {
|
|
const sortOrder = sortDescending ? -1 : 1;
|
|
|
|
return (
|
|
elementOne: { [x: string]: number | string },
|
|
elementTwo: { [x: string]: number | string }
|
|
) => {
|
|
return (
|
|
(elementOne[key] < elementTwo[key]
|
|
? -1
|
|
: elementOne[key] > elementTwo[key]
|
|
? 1
|
|
: 0) * sortOrder
|
|
);
|
|
};
|
|
};
|
|
|
|
export const isEven = (value: number): boolean => {
|
|
return value % 2 === 0;
|
|
};
|
|
|
|
export const getPartialNameFromFQN = (
|
|
fqn: string,
|
|
arrTypes: Array<'service' | 'database' | 'table' | 'column'> = [],
|
|
joinSeperator = '/'
|
|
): string => {
|
|
const arrFqn = Fqn.split(fqn);
|
|
const arrPartialName = [];
|
|
for (const type of arrTypes) {
|
|
if (type === 'service' && arrFqn.length > 0) {
|
|
arrPartialName.push(arrFqn[0]);
|
|
} else if (type === 'database' && arrFqn.length > 1) {
|
|
arrPartialName.push(arrFqn[1]);
|
|
} else if (type === 'table' && arrFqn.length > 2) {
|
|
arrPartialName.push(arrFqn[2]);
|
|
} else if (type === 'column' && arrFqn.length > 3) {
|
|
arrPartialName.push(arrFqn[3]);
|
|
}
|
|
}
|
|
|
|
return arrPartialName.join(joinSeperator);
|
|
};
|
|
|
|
export const getPartialNameFromTableFQN = (
|
|
fqn: string,
|
|
fqnParts: Array<FqnPart> = [],
|
|
joinSeparator = '/'
|
|
): string => {
|
|
if (!fqn) {
|
|
return '';
|
|
}
|
|
const splitFqn = Fqn.split(fqn);
|
|
// if nested column is requested, then ignore all the other
|
|
// parts and just return the nested column name
|
|
if (fqnParts.includes(FqnPart.NestedColumn)) {
|
|
// Remove the first 4 parts (service, database, schema, table)
|
|
|
|
return splitFqn.slice(4).join(FQN_SEPARATOR_CHAR);
|
|
}
|
|
const arrPartialName = [];
|
|
if (splitFqn.length > 0) {
|
|
if (fqnParts.includes(FqnPart.Service)) {
|
|
arrPartialName.push(splitFqn[0]);
|
|
}
|
|
if (fqnParts.includes(FqnPart.Database) && splitFqn.length > 1) {
|
|
arrPartialName.push(splitFqn[1]);
|
|
}
|
|
if (fqnParts.includes(FqnPart.Schema) && splitFqn.length > 2) {
|
|
arrPartialName.push(splitFqn[2]);
|
|
}
|
|
if (fqnParts.includes(FqnPart.Table) && splitFqn.length > 3) {
|
|
arrPartialName.push(splitFqn[3]);
|
|
}
|
|
if (fqnParts.includes(FqnPart.Column) && splitFqn.length > 4) {
|
|
arrPartialName.push(splitFqn[4]);
|
|
}
|
|
}
|
|
|
|
return arrPartialName.join(joinSeparator);
|
|
};
|
|
|
|
export const getTableFQNFromColumnFQN = (columnFQN: string): string => {
|
|
return getPartialNameFromTableFQN(
|
|
columnFQN,
|
|
[FqnPart.Service, FqnPart.Database, FqnPart.Schema, FqnPart.Table],
|
|
'.'
|
|
);
|
|
};
|
|
|
|
export const getCurrentUserId = (): string => {
|
|
const currentUser = AppState.getCurrentUserDetails();
|
|
|
|
return currentUser?.id || '';
|
|
};
|
|
|
|
export const pluralize = (count: number, noun: string, suffix = 's') => {
|
|
const countString = count.toLocaleString();
|
|
if (count !== 1 && count !== 0 && !noun.endsWith(suffix)) {
|
|
return `${countString} ${noun}${suffix}`;
|
|
} else {
|
|
if (noun.endsWith(suffix)) {
|
|
return `${countString} ${
|
|
count > 1 ? noun : noun.slice(0, noun.length - 1)
|
|
}`;
|
|
} else {
|
|
return `${countString} ${noun}${count > 1 ? suffix : ''}`;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const hasEditAccess = (type: string, id: string) => {
|
|
const loggedInUser = AppState.getCurrentUserDetails();
|
|
if (type === 'user') {
|
|
return id === loggedInUser?.id;
|
|
} else {
|
|
return Boolean(
|
|
loggedInUser?.teams?.length &&
|
|
loggedInUser?.teams?.some((team) => team.id === id)
|
|
);
|
|
}
|
|
};
|
|
|
|
export const getTabClasses = (
|
|
tab: number | string,
|
|
activeTab: number | string
|
|
) => {
|
|
return 'tw-gh-tabs' + (activeTab === tab ? ' active' : '');
|
|
};
|
|
|
|
export const getCountBadge = (
|
|
count = 0,
|
|
className = '',
|
|
isActive?: boolean
|
|
) => {
|
|
const clsBG = isUndefined(isActive)
|
|
? ''
|
|
: isActive
|
|
? 'tw-bg-primary tw-text-white tw-border-none'
|
|
: 'tw-bg-badge';
|
|
|
|
return (
|
|
<span
|
|
className={classNames(
|
|
'tw-py-px tw-px-1 tw-mx-1 tw-border tw-rounded tw-text-xs tw-min-w-badgeCount tw-text-center',
|
|
clsBG,
|
|
className
|
|
)}>
|
|
<span data-testid="filter-count" title={count.toString()}>
|
|
{count}
|
|
</span>
|
|
</span>
|
|
);
|
|
};
|
|
|
|
export const getRecentlyViewedData = (): Array<RecentlyViewedData> => {
|
|
const recentlyViewed: RecentlyViewed = reactLocalStorage.getObject(
|
|
LOCALSTORAGE_RECENTLY_VIEWED
|
|
) as RecentlyViewed;
|
|
|
|
if (recentlyViewed?.data) {
|
|
return recentlyViewed.data;
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
export const getRecentlySearchedData = (): Array<RecentlySearchedData> => {
|
|
const recentlySearch: RecentlySearched = reactLocalStorage.getObject(
|
|
LOCALSTORAGE_RECENTLY_SEARCHED
|
|
) as RecentlySearched;
|
|
if (recentlySearch?.data) {
|
|
return recentlySearch.data;
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
export const setRecentlyViewedData = (
|
|
recentData: Array<RecentlyViewedData>
|
|
): void => {
|
|
reactLocalStorage.setObject(LOCALSTORAGE_RECENTLY_VIEWED, {
|
|
data: recentData,
|
|
});
|
|
};
|
|
|
|
export const setRecentlySearchedData = (
|
|
recentData: Array<RecentlySearchedData>
|
|
): void => {
|
|
reactLocalStorage.setObject(LOCALSTORAGE_RECENTLY_SEARCHED, {
|
|
data: recentData,
|
|
});
|
|
};
|
|
|
|
export const addToRecentSearched = (searchTerm: string): void => {
|
|
if (searchTerm.trim()) {
|
|
const searchData = { term: searchTerm, timestamp: Date.now() };
|
|
const recentlySearch: RecentlySearched = reactLocalStorage.getObject(
|
|
LOCALSTORAGE_RECENTLY_SEARCHED
|
|
) as RecentlySearched;
|
|
let arrSearchedData: RecentlySearched['data'] = [];
|
|
if (recentlySearch?.data) {
|
|
const arrData = recentlySearch.data
|
|
// search term is not case-insensetive.
|
|
.filter((item) => item.term !== searchData.term)
|
|
.sort(
|
|
arraySorterByKey('timestamp', true) as (
|
|
a: RecentlySearchedData,
|
|
b: RecentlySearchedData
|
|
) => number
|
|
);
|
|
arrData.unshift(searchData);
|
|
|
|
if (arrData.length > 5) {
|
|
arrData.pop();
|
|
}
|
|
arrSearchedData = arrData;
|
|
} else {
|
|
arrSearchedData = [searchData];
|
|
}
|
|
setRecentlySearchedData(arrSearchedData);
|
|
}
|
|
};
|
|
|
|
export const removeRecentSearchTerm = (searchTerm: string) => {
|
|
const recentlySearch: RecentlySearched = reactLocalStorage.getObject(
|
|
LOCALSTORAGE_RECENTLY_SEARCHED
|
|
) as RecentlySearched;
|
|
if (recentlySearch?.data) {
|
|
const arrData = recentlySearch.data.filter(
|
|
(item) => item.term !== searchTerm
|
|
);
|
|
setRecentlySearchedData(arrData);
|
|
}
|
|
};
|
|
|
|
export const addToRecentViewed = (eData: RecentlyViewedData): void => {
|
|
const entityData = { ...eData, timestamp: Date.now() };
|
|
let recentlyViewed: RecentlyViewed = reactLocalStorage.getObject(
|
|
LOCALSTORAGE_RECENTLY_VIEWED
|
|
) as RecentlyViewed;
|
|
if (recentlyViewed?.data) {
|
|
const arrData = recentlyViewed.data
|
|
.filter((item) => item.fqn !== entityData.fqn)
|
|
.sort(
|
|
arraySorterByKey('timestamp', true) as (
|
|
a: RecentlyViewedData,
|
|
b: RecentlyViewedData
|
|
) => number
|
|
);
|
|
arrData.unshift(entityData);
|
|
|
|
if (arrData.length > 5) {
|
|
arrData.pop();
|
|
}
|
|
recentlyViewed.data = arrData;
|
|
} else {
|
|
recentlyViewed = {
|
|
data: [entityData],
|
|
};
|
|
}
|
|
setRecentlyViewedData(recentlyViewed.data);
|
|
};
|
|
|
|
export const getHtmlForNonAdminAction = (isClaimOwner: boolean) => {
|
|
return (
|
|
<>
|
|
<p>{TITLE_FOR_NON_OWNER_ACTION}</p>
|
|
{!isClaimOwner ? <p>Claim ownership in Manage </p> : null}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const getOwnerIds = (
|
|
filter: Ownership,
|
|
userDetails: User,
|
|
nonSecureUserDetails: User
|
|
): Array<string> => {
|
|
if (filter === Ownership.OWNER) {
|
|
if (!isEmpty(userDetails)) {
|
|
return [
|
|
...(userDetails.teams?.map((team) => team.id) || []),
|
|
userDetails.id,
|
|
];
|
|
} else {
|
|
if (!isEmpty(nonSecureUserDetails)) {
|
|
return [
|
|
...(nonSecureUserDetails.teams?.map((team) => team.id) || []),
|
|
nonSecureUserDetails.id,
|
|
];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
} else {
|
|
return [userDetails.id || nonSecureUserDetails.id];
|
|
}
|
|
};
|
|
|
|
export const getActiveCatClass = (name: string, activeName = '') => {
|
|
return activeName === name ? 'activeCategory' : '';
|
|
};
|
|
|
|
export const errorMsg = (value: string) => {
|
|
return (
|
|
<div className="tw-mt-1">
|
|
<strong
|
|
className="tw-text-red-500 tw-text-xs tw-italic"
|
|
data-testid="error-message">
|
|
{value}
|
|
</strong>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const validMsg = (value: string) => {
|
|
return (
|
|
<div className="tw-mt-1">
|
|
<strong
|
|
className="tw-text-success tw-text-xs tw-italic"
|
|
data-testid="error-message">
|
|
{value}
|
|
</strong>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const requiredField = (label: string, excludeSpace = false) => (
|
|
<>
|
|
{label}{' '}
|
|
<span className="tw-text-red-500">{!excludeSpace && <> </>}*</span>
|
|
</>
|
|
);
|
|
|
|
export const getSeparator = (
|
|
title: string | JSX.Element,
|
|
hrMarginTop = 'tw-mt-2.5'
|
|
) => {
|
|
return (
|
|
<span className="tw-flex tw-py-2 tw-text-grey-muted">
|
|
<hr className={classNames('tw-w-full', hrMarginTop)} />
|
|
{title && <span className="tw-px-0.5 tw-min-w-max">{title}</span>}
|
|
<hr className={classNames('tw-w-full', hrMarginTop)} />
|
|
</span>
|
|
);
|
|
};
|
|
|
|
export const getImages = (imageUri: string) => {
|
|
const imagesObj: typeof imageTypes = imageTypes;
|
|
for (const type in imageTypes) {
|
|
imagesObj[type as keyof typeof imageTypes] = imageUri.replace(
|
|
's96-c',
|
|
imageTypes[type as keyof typeof imageTypes]
|
|
);
|
|
}
|
|
|
|
return imagesObj;
|
|
};
|
|
|
|
export const getServiceLogo = (
|
|
serviceType: string,
|
|
className = ''
|
|
): JSX.Element | null => {
|
|
const logo = serviceTypeLogo(serviceType);
|
|
|
|
if (!isNull(logo)) {
|
|
return <img alt="" className={className} src={logo} />;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
export const getSvgArrow = (isActive: boolean) => {
|
|
return isActive ? (
|
|
<SVGIcons alt="arrow-down" icon={Icons.ARROW_DOWN_PRIMARY} />
|
|
) : (
|
|
<SVGIcons alt="arrow-right" icon={Icons.ARROW_RIGHT_PRIMARY} />
|
|
);
|
|
};
|
|
|
|
export const isValidUrl = (href?: string) => {
|
|
if (!href) {
|
|
return false;
|
|
}
|
|
try {
|
|
const url = new URL(href);
|
|
|
|
return Boolean(url.href);
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param email - email address string
|
|
* @returns - True|False
|
|
*/
|
|
export const isValidEmail = (email?: string) => {
|
|
let isValid = false;
|
|
if (email && email.match(validEmailRegEx)) {
|
|
isValid = true;
|
|
}
|
|
|
|
return isValid;
|
|
};
|
|
|
|
export const getFields = (defaultFields: string, tabSpecificField: string) => {
|
|
if (!tabSpecificField) {
|
|
return defaultFields;
|
|
}
|
|
if (!defaultFields) {
|
|
return tabSpecificField;
|
|
}
|
|
if (
|
|
tabSpecificField === TabSpecificField.LINEAGE ||
|
|
tabSpecificField === TabSpecificField.ACTIVITY_FEED
|
|
) {
|
|
return defaultFields;
|
|
}
|
|
|
|
return `${defaultFields}, ${tabSpecificField}`;
|
|
};
|
|
|
|
export const restrictFormSubmit = (e: FormEvent) => {
|
|
e.preventDefault();
|
|
};
|
|
|
|
export const getEntityMissingError = (entityType: string, fqn: string) => {
|
|
return (
|
|
<p>
|
|
{capitalize(entityType)} instance for <strong>{fqn}</strong> not found
|
|
</p>
|
|
);
|
|
};
|
|
|
|
export const getDocButton = (label: string, url: string, dataTestId = '') => {
|
|
return (
|
|
<Button
|
|
className="tw-group tw-rounded tw-w-full tw-px-3 tw-py-1.5 tw-text-sm"
|
|
data-testid={dataTestId}
|
|
href={url}
|
|
rel="noopener noreferrer"
|
|
size="custom"
|
|
tag="a"
|
|
target="_blank"
|
|
theme="primary"
|
|
variant="outlined">
|
|
<SVGIcons
|
|
alt="Doc icon"
|
|
className="tw-align-middle tw-mr-2 group-hover:tw-hidden"
|
|
icon={Icons.DOC_PRIMARY}
|
|
width="14"
|
|
/>
|
|
<SVGIcons
|
|
alt="Doc icon"
|
|
className="tw-align-middle tw-mr-2 tw-hidden group-hover:tw-inline-block"
|
|
icon={Icons.DOC_WHITE}
|
|
width="14"
|
|
/>
|
|
<span>{label}</span>
|
|
<SVGIcons
|
|
alt="external-link"
|
|
className="tw-align-middle tw-ml-2 group-hover:tw-hidden"
|
|
icon={Icons.EXTERNAL_LINK}
|
|
width="14"
|
|
/>
|
|
<SVGIcons
|
|
alt="external-link"
|
|
className="tw-align-middle tw-ml-2 tw-hidden group-hover:tw-inline-block"
|
|
icon={Icons.EXTERNAL_LINK_WHITE}
|
|
width="14"
|
|
/>
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
export const getNameFromFQN = (fqn: string): string => {
|
|
const arr = fqn.split(FQN_SEPARATOR_CHAR);
|
|
|
|
return arr[arr.length - 1];
|
|
};
|
|
|
|
export const getRandomColor = (name: string) => {
|
|
const firstAlphabet = name.charAt(0).toLowerCase();
|
|
const asciiCode = firstAlphabet.charCodeAt(0);
|
|
const colorNum =
|
|
asciiCode.toString() + asciiCode.toString() + asciiCode.toString();
|
|
|
|
const num = Math.round(0xffffff * parseInt(colorNum));
|
|
const r = (num >> 16) & 255;
|
|
const g = (num >> 8) & 255;
|
|
const b = num & 255;
|
|
|
|
return {
|
|
color: 'rgb(' + r + ', ' + g + ', ' + b + ', 0.6)',
|
|
character: firstAlphabet.toUpperCase(),
|
|
};
|
|
};
|
|
|
|
export const isUrlFriendlyName = (value: string) => {
|
|
return !UrlEntityCharRegEx.test(value);
|
|
};
|
|
|
|
/**
|
|
* Take teams data and filter out the non deleted teams
|
|
* @param teams - teams array
|
|
* @returns - non deleted team
|
|
*/
|
|
export const getNonDeletedTeams = (teams: EntityReference[]) => {
|
|
return teams.filter((t) => !t.deleted);
|
|
};
|
|
|
|
/**
|
|
* prepare label for given entity type and fqn
|
|
* @param type - entity type
|
|
* @param fqn - entity fqn
|
|
* @param withQuotes - boolean value
|
|
* @returns - label for entity
|
|
*/
|
|
export const prepareLabel = (type: string, fqn: string, withQuotes = true) => {
|
|
let label = '';
|
|
if (type === EntityType.TABLE) {
|
|
label = getPartialNameFromTableFQN(fqn, [FqnPart.Table]);
|
|
} else {
|
|
label = getPartialNameFromFQN(fqn, ['database']);
|
|
}
|
|
|
|
if (withQuotes) {
|
|
return label;
|
|
} else {
|
|
return label.replace(/(^"|"$)/g, '');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if entity is deleted and return with "(Deactivated) text"
|
|
* @param value - entity name
|
|
* @param isDeleted - boolean
|
|
* @returns - entity placeholder
|
|
*/
|
|
export const getEntityPlaceHolder = (value: string, isDeleted?: boolean) => {
|
|
if (isDeleted) {
|
|
return `${value} (Deactivated)`;
|
|
} else {
|
|
return value;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Take entity reference as input and return name for entity
|
|
* @param entity - entity reference
|
|
* @returns - entity name
|
|
*/
|
|
export const getEntityName = (
|
|
entity?:
|
|
| EntityReference
|
|
| ServicesType
|
|
| User
|
|
| Topic
|
|
| Database
|
|
| Dashboard
|
|
| Table
|
|
| Pipeline
|
|
| Team
|
|
| Policy
|
|
| Role
|
|
| GlossaryTerm
|
|
| Webhook
|
|
| Bot
|
|
) => {
|
|
return entity?.displayName || entity?.name || '';
|
|
};
|
|
|
|
export const getEntityId = (
|
|
entity?:
|
|
| EntityReference
|
|
| ServicesType
|
|
| User
|
|
| Topic
|
|
| Database
|
|
| Dashboard
|
|
| Table
|
|
| Pipeline
|
|
| Team
|
|
| Policy
|
|
| Role
|
|
) => entity?.id || '';
|
|
|
|
export const getEntityDeleteMessage = (entity: string, dependents: string) => {
|
|
if (dependents) {
|
|
return `Permanently deleting this ${getTitleCase(
|
|
entity
|
|
)} will remove its metadata, as well as the metadata of ${dependents} from OpenMetadata permanently.`;
|
|
} else {
|
|
return `Permanently deleting this ${getTitleCase(
|
|
entity
|
|
)} will remove its metadata from OpenMetadata permanently.`;
|
|
}
|
|
};
|
|
|
|
export const replaceSpaceWith_ = (text: string) => {
|
|
return text.replace(/\s/g, '_');
|
|
};
|
|
|
|
export const replaceAllSpacialCharWith_ = (text: string) => {
|
|
return text.replaceAll(/[&/\\#, +()$~%.'":*?<>{}]/g, '_');
|
|
};
|
|
|
|
export const getFeedCounts = (
|
|
entityType: string,
|
|
entityFQN: string,
|
|
conversationCallback: (
|
|
value: React.SetStateAction<EntityFieldThreadCount[]>
|
|
) => void,
|
|
taskCallback: (value: React.SetStateAction<EntityFieldThreadCount[]>) => void,
|
|
entityCallback: (value: React.SetStateAction<number>) => void
|
|
) => {
|
|
// To get conversation count
|
|
getFeedCount(
|
|
getEntityFeedLink(entityType, entityFQN),
|
|
ThreadType.Conversation
|
|
)
|
|
.then((res) => {
|
|
if (res) {
|
|
conversationCallback(res.counts);
|
|
} else {
|
|
throw jsonData['api-error-messages']['fetch-entity-feed-count-error'];
|
|
}
|
|
})
|
|
.catch((err: AxiosError) => {
|
|
showErrorToast(
|
|
err,
|
|
jsonData['api-error-messages']['fetch-entity-feed-count-error']
|
|
);
|
|
});
|
|
|
|
// To get open tasks count
|
|
getFeedCount(
|
|
getEntityFeedLink(entityType, entityFQN),
|
|
ThreadType.Task,
|
|
ThreadTaskStatus.Open
|
|
)
|
|
.then((res) => {
|
|
if (res) {
|
|
taskCallback(res.counts);
|
|
} else {
|
|
throw jsonData['api-error-messages']['fetch-entity-feed-count-error'];
|
|
}
|
|
})
|
|
.catch((err: AxiosError) => {
|
|
showErrorToast(
|
|
err,
|
|
jsonData['api-error-messages']['fetch-entity-feed-count-error']
|
|
);
|
|
});
|
|
|
|
// To get all thread count (task + conversation)
|
|
getFeedCount(getEntityFeedLink(entityType, entityFQN))
|
|
.then((res) => {
|
|
if (res) {
|
|
entityCallback(res.totalCount);
|
|
} else {
|
|
throw jsonData['api-error-messages']['fetch-entity-feed-count-error'];
|
|
}
|
|
})
|
|
.catch((err: AxiosError) => {
|
|
showErrorToast(
|
|
err,
|
|
jsonData['api-error-messages']['fetch-entity-feed-count-error']
|
|
);
|
|
});
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param entityType type of the entity
|
|
* @returns true if entity type exists in TASK_ENTITIES otherwise false
|
|
*/
|
|
export const isTaskSupported = (entityType: EntityType) =>
|
|
TASK_ENTITIES.includes(entityType);
|
|
|
|
/**
|
|
* Utility function to show pagination
|
|
* @param paging paging object
|
|
* @returns boolean
|
|
*/
|
|
export const showPagination = (paging: Paging) => {
|
|
return !isNil(paging.after) || !isNil(paging.before);
|
|
};
|
|
|
|
export const formatNumberWithComma = (number: number) => {
|
|
return new Intl.NumberFormat('en-US').format(number);
|
|
};
|
|
|
|
export const formTwoDigitNmber = (number: number) => {
|
|
return number.toLocaleString('en-US', {
|
|
minimumIntegerDigits: 2,
|
|
useGrouping: false,
|
|
});
|
|
};
|
|
|
|
export const getTeamsUser = (
|
|
data?: ExtraInfo
|
|
): Record<string, string | undefined> | undefined => {
|
|
if (!isUndefined(data) && !isEmpty(data?.placeholderText || data?.id)) {
|
|
const currentUser = AppState.getCurrentUserDetails();
|
|
const teams = currentUser?.teams;
|
|
|
|
const dataFound = teams?.find((team) => {
|
|
return data.id === team.id;
|
|
});
|
|
|
|
if (dataFound) {
|
|
return {
|
|
ownerName: (currentUser?.displayName || currentUser?.name) as string,
|
|
id: currentUser?.id as string,
|
|
};
|
|
}
|
|
}
|
|
|
|
return;
|
|
};
|
|
|
|
export const getDiffArray = (
|
|
compareWith: string[],
|
|
toCompare: string[]
|
|
): string[] => {
|
|
return differenceWith(compareWith, toCompare, isEqual);
|
|
};
|
|
|
|
export const getHostNameFromURL = (url: string) => {
|
|
if (isValidUrl(url)) {
|
|
const domain = new URL(url);
|
|
|
|
return domain.hostname;
|
|
} else {
|
|
return '';
|
|
}
|
|
};
|
|
|
|
export const commonUserDetailColumns: ColumnsType<User> = [
|
|
{
|
|
title: 'Username',
|
|
dataIndex: 'username',
|
|
key: 'username',
|
|
render: (_, record) => (
|
|
<Link
|
|
className="hover:tw-underline tw-cursor-pointer"
|
|
to={getUserPath(record.fullyQualifiedName || record.name)}>
|
|
{getEntityName(record)}
|
|
</Link>
|
|
),
|
|
},
|
|
{
|
|
title: 'Teams',
|
|
dataIndex: 'teams',
|
|
key: 'teams',
|
|
render: (_, record) => {
|
|
const listLength = record.teams?.length ?? 0;
|
|
const hasMore = listLength > LIST_CAP;
|
|
|
|
if (isUndefined(record.teams) || isEmpty(record.teams)) {
|
|
return <>No Team</>;
|
|
} else {
|
|
return (
|
|
<Space wrap data-testid="policy-link" size={4}>
|
|
{record.teams.slice(0, LIST_CAP).map((team) => (
|
|
<Link
|
|
className="hover:tw-underline tw-cursor-pointer"
|
|
key={uniqueId()}
|
|
to={getTeamsWithFqnPath(team.fullyQualifiedName ?? '')}>
|
|
{getEntityName(team)}
|
|
</Link>
|
|
))}
|
|
{hasMore && (
|
|
<Popover
|
|
className="tw-cursor-pointer"
|
|
content={
|
|
<Space wrap size={4}>
|
|
{record.teams.slice(LIST_CAP).map((team) => (
|
|
<Link
|
|
className="hover:tw-underline tw-cursor-pointer"
|
|
key={uniqueId()}
|
|
to={getTeamsWithFqnPath(team.fullyQualifiedName ?? '')}>
|
|
{getEntityName(team)}
|
|
</Link>
|
|
))}
|
|
</Space>
|
|
}
|
|
overlayClassName="tw-w-40 tw-text-center"
|
|
trigger="click">
|
|
<Tag className="tw-ml-1" data-testid="plus-more-count">{`+${
|
|
listLength - LIST_CAP
|
|
} more`}</Tag>
|
|
</Popover>
|
|
)}
|
|
</Space>
|
|
);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: 'Roles',
|
|
dataIndex: 'roles',
|
|
key: 'roles',
|
|
render: (_, record) => {
|
|
const listLength = record.roles?.length ?? 0;
|
|
const hasMore = listLength > LIST_CAP;
|
|
|
|
if (isUndefined(record.roles) || isEmpty(record.roles)) {
|
|
return <>No Role</>;
|
|
} else {
|
|
return (
|
|
<Space wrap data-testid="policy-link" size={4}>
|
|
{record.roles.slice(0, LIST_CAP).map((role) => (
|
|
<Link
|
|
className="hover:tw-underline tw-cursor-pointer"
|
|
key={uniqueId()}
|
|
to={getRoleWithFqnPath(role.fullyQualifiedName ?? '')}>
|
|
{getEntityName(role)}
|
|
</Link>
|
|
))}
|
|
{hasMore && (
|
|
<Popover
|
|
className="tw-cursor-pointer"
|
|
content={
|
|
<Space wrap size={4}>
|
|
{record.roles.slice(LIST_CAP).map((role) => (
|
|
<Link
|
|
className="hover:tw-underline tw-cursor-pointer"
|
|
key={uniqueId()}
|
|
to={getRoleWithFqnPath(role.fullyQualifiedName ?? '')}>
|
|
{getEntityName(role)}
|
|
</Link>
|
|
))}
|
|
</Space>
|
|
}
|
|
overlayClassName="tw-w-40 tw-text-center"
|
|
trigger="click">
|
|
<Tag className="tw-ml-1" data-testid="plus-more-count">{`+${
|
|
listLength - LIST_CAP
|
|
} more`}</Tag>
|
|
</Popover>
|
|
)}
|
|
</Space>
|
|
);
|
|
}
|
|
},
|
|
},
|
|
];
|
|
|
|
export const getOwnerValue = (owner: EntityReference) => {
|
|
switch (owner?.type) {
|
|
case 'team':
|
|
return getTeamAndUserDetailsPath(owner?.name || '');
|
|
case 'user':
|
|
return getUserPath(owner?.fullyQualifiedName ?? '');
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
export const getIngestionFrequency = (pipelineType: PipelineType) => {
|
|
const value = {
|
|
min: 0,
|
|
hour: 0,
|
|
};
|
|
|
|
switch (pipelineType) {
|
|
case PipelineType.TestSuite:
|
|
case PipelineType.Metadata:
|
|
return getHourCron(value);
|
|
|
|
default:
|
|
return getDayCron(value);
|
|
}
|
|
};
|
|
|
|
export const getEmptyPlaceholder = () => {
|
|
return (
|
|
<ErrorPlaceHolder size={SIZE.MEDIUM}>
|
|
<Typography.Paragraph>
|
|
{i18n.t('label.no-data-available')}
|
|
</Typography.Paragraph>
|
|
</ErrorPlaceHolder>
|
|
);
|
|
};
|
|
|
|
// return the status like loading and success
|
|
export const getLoadingStatus = (
|
|
current: CurrentState,
|
|
id: string | undefined,
|
|
displayText: string
|
|
) => {
|
|
return current.id === id ? (
|
|
current.state === 'success' ? (
|
|
<FontAwesomeIcon icon="check" />
|
|
) : (
|
|
<Loader size="small" type="default" />
|
|
)
|
|
) : (
|
|
displayText
|
|
);
|
|
};
|
|
|
|
// return array of id as strings
|
|
export const getEntityIdArray = (entities: EntityReference[]): string[] =>
|
|
entities.map((item) => item.id);
|
|
|
|
export const getTierFromEntityInfo = (entity: FormattedTableData) => {
|
|
return (
|
|
entity.tier?.tagFQN ||
|
|
getTierFromSearchTableTags((entity.tags || []).map((tag) => tag.tagFQN))
|
|
)?.split(FQN_SEPARATOR_CHAR)[1];
|
|
};
|