mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-28 02:46:09 +00:00
* Fix #4293 UI: Add support for Delete `recursive=true` for given entities. * Remove tooltip from entity feed card header * pass recursive if its not undefined. * Chnage confirmation text to `DELETE` * Add deleteEntity message support * Add support for database and database schema count * Add support for delete entity message * Fix Faling test * Remove unwanted code * Add unit test * Addressing review comment
This commit is contained in:
parent
e4e1d4971b
commit
c7c3d153e9
@ -12,8 +12,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { isUndefined } from 'lodash';
|
||||||
import { Edge } from '../components/EntityLineage/EntityLineage.interface';
|
import { Edge } from '../components/EntityLineage/EntityLineage.interface';
|
||||||
import { SearchIndex } from '../enums/search.enum';
|
import { SearchIndex } from '../enums/search.enum';
|
||||||
|
import { getURLWithQueryFields } from '../utils/APIUtils';
|
||||||
import { getCurrentUserId } from '../utils/CommonUtils';
|
import { getCurrentUserId } from '../utils/CommonUtils';
|
||||||
import { getSearchAPIQuery } from '../utils/SearchUtils';
|
import { getSearchAPIQuery } from '../utils/SearchUtils';
|
||||||
import APIClient from './index';
|
import APIClient from './index';
|
||||||
@ -116,7 +118,18 @@ export const getInitialEntity: Function = (): Promise<AxiosResponse> => {
|
|||||||
|
|
||||||
export const deleteEntity: Function = (
|
export const deleteEntity: Function = (
|
||||||
entityType: string,
|
entityType: string,
|
||||||
entityId: string
|
entityId: string,
|
||||||
|
isRecursive: boolean
|
||||||
): Promise<AxiosResponse> => {
|
): Promise<AxiosResponse> => {
|
||||||
return APIClient.delete(`/${entityType}/${entityId}?hardDelete=true`);
|
const searchParams = new URLSearchParams({ hardDelete: `true` });
|
||||||
|
if (!isUndefined(isRecursive)) {
|
||||||
|
searchParams.set('recursive', `${isRecursive}`);
|
||||||
|
}
|
||||||
|
const path = getURLWithQueryFields(
|
||||||
|
`/${entityType}/${entityId}`,
|
||||||
|
'',
|
||||||
|
`${searchParams.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return APIClient.delete(path);
|
||||||
};
|
};
|
||||||
|
@ -26,6 +26,7 @@ import { getCategory } from '../../axiosAPIs/tagAPI';
|
|||||||
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
||||||
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
|
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
|
||||||
import { ENTITY_DELETE_STATE } from '../../constants/entity.constants';
|
import { ENTITY_DELETE_STATE } from '../../constants/entity.constants';
|
||||||
|
import { EntityType } from '../../enums/entity.enum';
|
||||||
import { Operation } from '../../generated/entity/policies/accessControl/rule';
|
import { Operation } from '../../generated/entity/policies/accessControl/rule';
|
||||||
import { useAuth } from '../../hooks/authHooks';
|
import { useAuth } from '../../hooks/authHooks';
|
||||||
import jsonData from '../../jsons/en';
|
import jsonData from '../../jsons/en';
|
||||||
@ -53,6 +54,8 @@ const ManageTab: FunctionComponent<ManageProps> = ({
|
|||||||
entityName,
|
entityName,
|
||||||
entityType,
|
entityType,
|
||||||
entityId,
|
entityId,
|
||||||
|
isRecursiveDelete,
|
||||||
|
deletEntityMessage,
|
||||||
}: ManageProps) => {
|
}: ManageProps) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { userPermissions, isAdminUser } = useAuth();
|
const { userPermissions, isAdminUser } = useAuth();
|
||||||
@ -166,9 +169,28 @@ const ManageTab: FunctionComponent<ManageProps> = ({
|
|||||||
setEntityDeleteState(ENTITY_DELETE_STATE);
|
setEntityDeleteState(ENTITY_DELETE_STATE);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const prepareEntityType = () => {
|
||||||
|
const services = [
|
||||||
|
EntityType.DASHBOARD_SERVICE,
|
||||||
|
EntityType.DATABASE_SERVICE,
|
||||||
|
EntityType.MESSAGING_SERVICE,
|
||||||
|
EntityType.PIPELINE_SERVICE,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (services.includes((entityType || '') as EntityType)) {
|
||||||
|
return `services/${entityType}s`;
|
||||||
|
} else {
|
||||||
|
return `${entityType}s`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareDeleteMessage = () => {
|
||||||
|
return `Once you delete this ${entityType}, it will be removed permanently`;
|
||||||
|
};
|
||||||
|
|
||||||
const handleOnEntityDeleteConfirm = () => {
|
const handleOnEntityDeleteConfirm = () => {
|
||||||
setEntityDeleteState((prev) => ({ ...prev, loading: 'waiting' }));
|
setEntityDeleteState((prev) => ({ ...prev, loading: 'waiting' }));
|
||||||
deleteEntity(`${entityType}s`, entityId)
|
deleteEntity(prepareEntityType(), entityId, isRecursiveDelete)
|
||||||
.then((res: AxiosResponse) => {
|
.then((res: AxiosResponse) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -201,6 +223,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
|
|||||||
if (allowDelete && entityDeleteState.state) {
|
if (allowDelete && entityDeleteState.state) {
|
||||||
return (
|
return (
|
||||||
<EntityDeleteModal
|
<EntityDeleteModal
|
||||||
|
bodyText={deletEntityMessage || prepareDeleteMessage()}
|
||||||
entityName={entityName as string}
|
entityName={entityName as string}
|
||||||
entityType={entityType as string}
|
entityType={entityType as string}
|
||||||
loadingState={entityDeleteState.loading}
|
loadingState={entityDeleteState.loading}
|
||||||
@ -222,9 +245,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
|
|||||||
<h4 className="tw-text-base" data-testid="danger-zone-text-title">
|
<h4 className="tw-text-base" data-testid="danger-zone-text-title">
|
||||||
Delete {entityType} {entityName}
|
Delete {entityType} {entityName}
|
||||||
</h4>
|
</h4>
|
||||||
<p data-testid="danger-zone-text-para">
|
<p data-testid="danger-zone-text-para">{prepareDeleteMessage()}</p>
|
||||||
{`Once you delete this ${entityType}, it would be removed permanently`}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<NonAdminAction
|
<NonAdminAction
|
||||||
className="tw-self-center"
|
className="tw-self-center"
|
||||||
|
@ -29,6 +29,8 @@ export interface ManageProps {
|
|||||||
entityName?: string;
|
entityName?: string;
|
||||||
entityType?: string;
|
entityType?: string;
|
||||||
allowDelete?: boolean;
|
allowDelete?: boolean;
|
||||||
|
isRecursiveDelete?: boolean;
|
||||||
|
deletEntityMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Status = 'initial' | 'waiting' | 'success';
|
export type Status = 'initial' | 'waiting' | 'success';
|
||||||
|
@ -96,7 +96,7 @@ describe('Test EntityDelete Modal Component', () => {
|
|||||||
const inputBox = await findByTestId(container, 'confirmation-text-input');
|
const inputBox = await findByTestId(container, 'confirmation-text-input');
|
||||||
|
|
||||||
fireEvent.change(inputBox, {
|
fireEvent.change(inputBox, {
|
||||||
target: { value: `${mockProp.entityType}/${mockProp.entityName}` },
|
target: { value: 'DELETE' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(confirmButton).not.toBeDisabled();
|
expect(confirmButton).not.toBeDisabled();
|
||||||
|
@ -28,6 +28,7 @@ interface Prop extends HTMLAttributes<HTMLDivElement> {
|
|||||||
entityName: string;
|
entityName: string;
|
||||||
entityType: string;
|
entityType: string;
|
||||||
loadingState: string;
|
loadingState: string;
|
||||||
|
bodyText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntityDeleteModal: FC<Prop> = ({
|
const EntityDeleteModal: FC<Prop> = ({
|
||||||
@ -37,6 +38,7 @@ const EntityDeleteModal: FC<Prop> = ({
|
|||||||
entityType,
|
entityType,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
bodyText,
|
||||||
}: Prop) => {
|
}: Prop) => {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
@ -45,7 +47,7 @@ const EntityDeleteModal: FC<Prop> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isNameMatching = useCallback(() => {
|
const isNameMatching = useCallback(() => {
|
||||||
return name === `${entityType}/${entityName}`;
|
return name === 'DELETE';
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -60,13 +62,12 @@ const EntityDeleteModal: FC<Prop> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames('tw-modal-body')} data-testid="body-text">
|
<div className={classNames('tw-modal-body')} data-testid="body-text">
|
||||||
<p className="tw-mb-2">{`Once you delete this ${entityType}, it would be removed permanently`}</p>
|
|
||||||
<p className="tw-mb-2">
|
<p className="tw-mb-2">
|
||||||
Type{' '}
|
{bodyText ||
|
||||||
<strong>
|
`Once you delete this ${entityType}, it will be removed permanently`}
|
||||||
{entityType}/{entityName}
|
</p>
|
||||||
</strong>{' '}
|
<p className="tw-mb-2">
|
||||||
to confirm
|
Type <strong>DELETE</strong> to confirm
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
@ -26,7 +26,6 @@ import { TableType } from '../../../generated/entity/data/table';
|
|||||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||||
import { serviceTypeLogo } from '../../../utils/ServiceUtils';
|
import { serviceTypeLogo } from '../../../utils/ServiceUtils';
|
||||||
import { getEntityLink, getUsagePercentile } from '../../../utils/TableUtils';
|
import { getEntityLink, getUsagePercentile } from '../../../utils/TableUtils';
|
||||||
import PopOver from '../popover/PopOver';
|
|
||||||
import TableDataCardBody from './TableDataCardBody';
|
import TableDataCardBody from './TableDataCardBody';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -63,9 +62,6 @@ const TableDataCard: FunctionComponent<Props> = ({
|
|||||||
indexType,
|
indexType,
|
||||||
matches,
|
matches,
|
||||||
tableType,
|
tableType,
|
||||||
service,
|
|
||||||
database,
|
|
||||||
databaseSchema,
|
|
||||||
deleted = false,
|
deleted = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -116,45 +112,6 @@ const TableDataCard: FunctionComponent<Props> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPopOverContent = () => {
|
|
||||||
const entityDetails = [
|
|
||||||
{
|
|
||||||
key: 'Service Type',
|
|
||||||
value: serviceType,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (service) {
|
|
||||||
entityDetails.push({
|
|
||||||
key: 'Service',
|
|
||||||
value: service,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (database) {
|
|
||||||
entityDetails.push({
|
|
||||||
key: 'Database',
|
|
||||||
value: database,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (databaseSchema) {
|
|
||||||
entityDetails.push({
|
|
||||||
key: 'Schema',
|
|
||||||
value: databaseSchema,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="tw-text-left">
|
|
||||||
{entityDetails.map((detail) => (
|
|
||||||
<p key={detail.key}>
|
|
||||||
<span className="tw-text-grey-muted">{detail.key} : </span>
|
|
||||||
<span className="tw-ml-2">{detail.value}</span>
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md"
|
className="tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md"
|
||||||
@ -167,21 +124,15 @@ const TableDataCard: FunctionComponent<Props> = ({
|
|||||||
className="tw-inline tw-h-5 tw-w-5"
|
className="tw-inline tw-h-5 tw-w-5"
|
||||||
src={serviceTypeLogo(serviceType || '')}
|
src={serviceTypeLogo(serviceType || '')}
|
||||||
/>
|
/>
|
||||||
<PopOver
|
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">
|
||||||
html={getPopOverContent()}
|
<button
|
||||||
position="top"
|
className="tw-text-grey-body tw-font-medium"
|
||||||
theme="light"
|
data-testid="table-link"
|
||||||
trigger="mouseenter">
|
id={`${id}Title`}
|
||||||
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">
|
onClick={handleLinkClick}>
|
||||||
<button
|
{fullyQualifiedName}
|
||||||
className="tw-text-grey-body tw-font-medium"
|
</button>
|
||||||
data-testid="table-link"
|
</h6>
|
||||||
id={`${id}Title`}
|
|
||||||
onClick={handleLinkClick}>
|
|
||||||
{fullyQualifiedName}
|
|
||||||
</button>
|
|
||||||
</h6>
|
|
||||||
</PopOver>
|
|
||||||
{deleted && (
|
{deleted && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
@ -75,6 +75,7 @@ import {
|
|||||||
getPartialNameFromTableFQN,
|
getPartialNameFromTableFQN,
|
||||||
hasEditAccess,
|
hasEditAccess,
|
||||||
isEven,
|
isEven,
|
||||||
|
pluralize,
|
||||||
} from '../../utils/CommonUtils';
|
} from '../../utils/CommonUtils';
|
||||||
import {
|
import {
|
||||||
databaseSchemaDetailsTabs,
|
databaseSchemaDetailsTabs,
|
||||||
@ -566,6 +567,18 @@ const DatabaseSchemaPage: FunctionComponent = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDeleteEntityMessage = () => {
|
||||||
|
if (!tableInstanceCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Deleting this databaseSchema will also delete ${pluralize(
|
||||||
|
tableInstanceCount,
|
||||||
|
'table',
|
||||||
|
's'
|
||||||
|
)}.`;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (TabSpecificField.ACTIVITY_FEED === tab) {
|
if (TabSpecificField.ACTIVITY_FEED === tab) {
|
||||||
fetchActivityFeed();
|
fetchActivityFeed();
|
||||||
@ -675,7 +688,9 @@ const DatabaseSchemaPage: FunctionComponent = () => {
|
|||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
allowDelete
|
allowDelete
|
||||||
hideTier
|
hideTier
|
||||||
|
isRecursiveDelete
|
||||||
currentUser={databaseSchema?.owner?.id}
|
currentUser={databaseSchema?.owner?.id}
|
||||||
|
deletEntityMessage={getDeleteEntityMessage()}
|
||||||
entityId={databaseSchema?.id}
|
entityId={databaseSchema?.id}
|
||||||
entityName={databaseSchema?.name}
|
entityName={databaseSchema?.name}
|
||||||
entityType={EntityType.DATABASE_SCHEMA}
|
entityType={EntityType.DATABASE_SCHEMA}
|
||||||
|
@ -38,6 +38,7 @@ import {
|
|||||||
postFeedById,
|
postFeedById,
|
||||||
postThread,
|
postThread,
|
||||||
} from '../../axiosAPIs/feedsAPI';
|
} from '../../axiosAPIs/feedsAPI';
|
||||||
|
import { getAllTables } from '../../axiosAPIs/tableAPI';
|
||||||
import ActivityFeedList from '../../components/ActivityFeed/ActivityFeedList/ActivityFeedList';
|
import ActivityFeedList from '../../components/ActivityFeed/ActivityFeedList/ActivityFeedList';
|
||||||
import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
|
import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
|
||||||
import Description from '../../components/common/description/Description';
|
import Description from '../../components/common/description/Description';
|
||||||
@ -71,7 +72,12 @@ import { EntityReference } from '../../generated/entity/teams/user';
|
|||||||
import { Paging } from '../../generated/type/paging';
|
import { Paging } from '../../generated/type/paging';
|
||||||
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll';
|
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll';
|
||||||
import jsonData from '../../jsons/en';
|
import jsonData from '../../jsons/en';
|
||||||
import { getEntityName, hasEditAccess, isEven } from '../../utils/CommonUtils';
|
import {
|
||||||
|
getEntityName,
|
||||||
|
hasEditAccess,
|
||||||
|
isEven,
|
||||||
|
pluralize,
|
||||||
|
} from '../../utils/CommonUtils';
|
||||||
import {
|
import {
|
||||||
databaseDetailsTabs,
|
databaseDetailsTabs,
|
||||||
getCurrentDatabaseDetailsTab,
|
getCurrentDatabaseDetailsTab,
|
||||||
@ -109,6 +115,7 @@ const DatabaseDetails: FunctionComponent = () => {
|
|||||||
useState<Paging>(pagingObject);
|
useState<Paging>(pagingObject);
|
||||||
const [databaseSchemaInstanceCount, setSchemaInstanceCount] =
|
const [databaseSchemaInstanceCount, setSchemaInstanceCount] =
|
||||||
useState<number>(0);
|
useState<number>(0);
|
||||||
|
const [tableInstanceCount, setTableInstanceCount] = useState<number>(0);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<number>(
|
const [activeTab, setActiveTab] = useState<number>(
|
||||||
getCurrentDatabaseDetailsTab(tab)
|
getCurrentDatabaseDetailsTab(tab)
|
||||||
@ -521,6 +528,25 @@ const DatabaseDetails: FunctionComponent = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchTablesCount = () => {
|
||||||
|
// limit=0 will fetch empty data list with total count
|
||||||
|
getAllTables('', 0)
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
if (res.data) {
|
||||||
|
setTableInstanceCount(res.data.paging.total);
|
||||||
|
} else {
|
||||||
|
throw jsonData['api-error-messages']['unexpected-server-response'];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
showErrorToast(
|
||||||
|
err,
|
||||||
|
jsonData['api-error-messages']['unexpected-server-response']
|
||||||
|
);
|
||||||
|
setTableInstanceCount(0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getLoader = () => {
|
const getLoader = () => {
|
||||||
return isentityThreadLoading ? <Loader /> : null;
|
return isentityThreadLoading ? <Loader /> : null;
|
||||||
};
|
};
|
||||||
@ -535,8 +561,21 @@ const DatabaseDetails: FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDeleteEntityMessage = () => {
|
||||||
|
if (!databaseSchemaInstanceCount && !tableInstanceCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Deleting this database will also delete ${pluralize(
|
||||||
|
databaseSchemaInstanceCount,
|
||||||
|
'schema',
|
||||||
|
's'
|
||||||
|
)} and ${pluralize(tableInstanceCount, 'table', 's')}.`;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getEntityFeedCount();
|
getEntityFeedCount();
|
||||||
|
fetchTablesCount();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -758,7 +797,9 @@ const DatabaseDetails: FunctionComponent = () => {
|
|||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
allowDelete
|
allowDelete
|
||||||
hideTier
|
hideTier
|
||||||
|
isRecursiveDelete
|
||||||
currentUser={database?.owner?.id}
|
currentUser={database?.owner?.id}
|
||||||
|
deletEntityMessage={getDeleteEntityMessage()}
|
||||||
entityId={database?.id}
|
entityId={database?.id}
|
||||||
entityName={database?.name}
|
entityName={database?.name}
|
||||||
entityType={EntityType.DATABASE}
|
entityType={EntityType.DATABASE}
|
||||||
|
@ -975,10 +975,11 @@ const ServicePage: FunctionComponent = () => {
|
|||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
allowDelete
|
allowDelete
|
||||||
hideTier
|
hideTier
|
||||||
|
isRecursiveDelete
|
||||||
currentUser={serviceDetails?.owner?.id}
|
currentUser={serviceDetails?.owner?.id}
|
||||||
entityId={serviceDetails?.id}
|
entityId={serviceDetails?.id}
|
||||||
entityName={serviceDetails?.name}
|
entityName={serviceDetails?.name}
|
||||||
entityType={`services/${serviceCategory.slice(0, -1)}`}
|
entityType={serviceCategory.slice(0, -1)}
|
||||||
hasEditAccess={hasEditAccess(
|
hasEditAccess={hasEditAccess(
|
||||||
serviceDetails?.owner?.type || '',
|
serviceDetails?.owner?.type || '',
|
||||||
serviceDetails?.owner?.id || ''
|
serviceDetails?.owner?.id || ''
|
||||||
|
Loading…
x
Reference in New Issue
Block a user