mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-25 17:08:29 +00:00
feat(ui): Introducing flows for creating and deleting manage tags (#13107)
This commit is contained in:
parent
4df375df32
commit
4e30fa78ed
@ -0,0 +1,152 @@
|
||||
import React, { useState } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { Modal } from '@components';
|
||||
import { useCreateTagMutation } from '../../../graphql/tag.generated';
|
||||
import { useEnterKeyListener } from '../../shared/useEnterKeyListener';
|
||||
import { useBatchAddOwnersMutation, useSetTagColorMutation } from '../../../graphql/mutations.generated';
|
||||
import { ModalButton } from './types';
|
||||
import TagDetailsSection from './TagDetailsSection';
|
||||
import OwnersSection, { PendingOwner } from './OwnersSection';
|
||||
|
||||
type CreateNewTagModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modal for creating a new tag with owners and applying it to entities
|
||||
*/
|
||||
const CreateNewTagModal: React.FC<CreateNewTagModalProps> = ({ onClose, open }) => {
|
||||
// Tag details state
|
||||
const [tagName, setTagName] = useState('');
|
||||
const [tagDescription, setTagDescription] = useState('');
|
||||
const [tagColor, setTagColor] = useState('#1890ff');
|
||||
|
||||
// Owners state
|
||||
const [selectedOwnerUrns, setSelectedOwnerUrns] = useState<string[]>([]);
|
||||
const [pendingOwners, setPendingOwners] = useState<PendingOwner[]>([]);
|
||||
|
||||
// Loading state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Mutations
|
||||
const [createTagMutation] = useCreateTagMutation();
|
||||
const [setTagColorMutation] = useSetTagColorMutation();
|
||||
const [batchAddOwnersMutation] = useBatchAddOwnersMutation();
|
||||
|
||||
/**
|
||||
* Handler for creating the tag and applying it to entities
|
||||
*/
|
||||
const onOk = async () => {
|
||||
if (!tagName) {
|
||||
message.error('Tag name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Step 1: Create the new tag
|
||||
const createTagResult = await createTagMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: tagName.trim(),
|
||||
name: tagName.trim(),
|
||||
description: tagDescription,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newTagUrn = createTagResult.data?.createTag;
|
||||
|
||||
if (!newTagUrn) {
|
||||
message.error('Failed to create tag. An unexpected error occurred');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Add color
|
||||
if (tagColor) {
|
||||
await setTagColorMutation({
|
||||
variables: {
|
||||
urn: newTagUrn,
|
||||
colorHex: tagColor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: Add owners if any
|
||||
if (pendingOwners.length > 0) {
|
||||
await batchAddOwnersMutation({
|
||||
variables: {
|
||||
input: {
|
||||
owners: pendingOwners,
|
||||
resources: [{ resourceUrn: newTagUrn }],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
message.success(`Tag "${tagName}" successfully created`);
|
||||
onClose();
|
||||
setTagName('');
|
||||
setTagDescription('');
|
||||
setTagColor('#1890ff');
|
||||
setSelectedOwnerUrns([]);
|
||||
setPendingOwners([]);
|
||||
} catch (e: any) {
|
||||
message.destroy();
|
||||
message.error('Failed to create tag. An unexpected error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the Enter press
|
||||
useEnterKeyListener({
|
||||
querySelectorToExecuteClick: '#createNewTagButton',
|
||||
});
|
||||
|
||||
// Modal buttons configuration
|
||||
const buttons: ModalButton[] = [
|
||||
{
|
||||
text: 'Cancel',
|
||||
color: 'violet',
|
||||
variant: 'text',
|
||||
onClick: onClose,
|
||||
},
|
||||
{
|
||||
text: 'Create',
|
||||
id: 'createNewTagButton',
|
||||
color: 'violet',
|
||||
variant: 'filled',
|
||||
onClick: onOk,
|
||||
disabled: !tagName || isLoading,
|
||||
isLoading,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal title="Create New Tag" onCancel={onClose} buttons={buttons} open={open} centered width={500}>
|
||||
{/* Tag Details Section */}
|
||||
<TagDetailsSection
|
||||
tagName={tagName}
|
||||
setTagName={setTagName}
|
||||
tagDescription={tagDescription}
|
||||
setTagDescription={setTagDescription}
|
||||
tagColor={tagColor}
|
||||
setTagColor={setTagColor}
|
||||
/>
|
||||
|
||||
{/* Owners Section */}
|
||||
<OwnersSection
|
||||
selectedOwnerUrns={selectedOwnerUrns}
|
||||
setSelectedOwnerUrns={setSelectedOwnerUrns}
|
||||
pendingOwners={pendingOwners}
|
||||
setPendingOwners={setPendingOwners}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateNewTagModal;
|
||||
@ -0,0 +1,199 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Select } from 'antd';
|
||||
import { Text } from '@components';
|
||||
import { EntityType, OwnerEntityType } from '../../../types.generated';
|
||||
import { useGetSearchResultsForMultipleLazyQuery } from '../../../graphql/search.generated';
|
||||
import { useEntityRegistry } from '../../useEntityRegistry';
|
||||
import { useListOwnershipTypesQuery } from '../../../graphql/ownership.generated';
|
||||
import { OwnerLabel } from '../../shared/OwnerLabel';
|
||||
|
||||
// Interface for pending owner
|
||||
export interface PendingOwner {
|
||||
ownerUrn: string;
|
||||
ownerEntityType: OwnerEntityType;
|
||||
ownershipTypeUrn: string;
|
||||
}
|
||||
|
||||
// Owners section props
|
||||
export interface OwnersSectionProps {
|
||||
selectedOwnerUrns: string[];
|
||||
setSelectedOwnerUrns: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
pendingOwners: PendingOwner[];
|
||||
setPendingOwners: React.Dispatch<React.SetStateAction<PendingOwner[]>>;
|
||||
}
|
||||
|
||||
const SectionContainer = styled.div`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const SectionHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const FormSection = styled.div`
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const SelectInput = styled(Select)`
|
||||
width: 100%;
|
||||
|
||||
.ant-select-selection-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 16px;
|
||||
margin: 2px;
|
||||
height: 32px;
|
||||
padding-left: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-select-selection-item-remove {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Component for owner selection and management
|
||||
*/
|
||||
const OwnersSection: React.FC<OwnersSectionProps> = ({
|
||||
selectedOwnerUrns,
|
||||
setSelectedOwnerUrns,
|
||||
pendingOwners,
|
||||
setPendingOwners,
|
||||
}) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
// Search query for owners across both CorpUser and CorpGroup types
|
||||
const [searchAcrossEntities, { data: searchData, loading: searchLoading }] =
|
||||
useGetSearchResultsForMultipleLazyQuery({
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
// Lazy load ownership types
|
||||
const { data: ownershipTypesData } = useListOwnershipTypesQuery({
|
||||
variables: {
|
||||
input: {},
|
||||
},
|
||||
});
|
||||
|
||||
const ownershipTypes = ownershipTypesData?.listOwnershipTypes?.ownershipTypes || [];
|
||||
const defaultOwnerType = ownershipTypes.length > 0 ? ownershipTypes[0].urn : undefined;
|
||||
|
||||
// Get search results from the combined query
|
||||
const searchResults = searchData?.searchAcrossEntities?.searchResults?.map((result) => result.entity) || [];
|
||||
|
||||
// Debounced search handler
|
||||
const handleOwnerSearch = (text: string) => {
|
||||
setInputValue(text.trim());
|
||||
setIsSearching(true);
|
||||
|
||||
if (text && text.trim().length > 1) {
|
||||
searchAcrossEntities({
|
||||
variables: {
|
||||
input: {
|
||||
types: [EntityType.CorpUser, EntityType.CorpGroup],
|
||||
query: text.trim(),
|
||||
start: 0,
|
||||
count: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Renders a search result in the select dropdown
|
||||
const renderSearchResult = (entityItem: any) => {
|
||||
const avatarUrl =
|
||||
entityItem.type === EntityType.CorpUser ? entityItem.editableProperties?.pictureLink : undefined;
|
||||
const displayName = entityRegistry.getDisplayName(entityItem.type, entityItem);
|
||||
|
||||
return (
|
||||
<Select.Option
|
||||
key={entityItem.urn}
|
||||
value={entityItem.urn}
|
||||
label={<OwnerLabel name={displayName} avatarUrl={avatarUrl} type={entityItem.type} />}
|
||||
>
|
||||
<OwnerLabel name={displayName} avatarUrl={avatarUrl} type={entityItem.type} />
|
||||
</Select.Option>
|
||||
);
|
||||
};
|
||||
|
||||
// Handle select change - stores owners as pending until save
|
||||
const handleSelectChange = (values: any) => {
|
||||
const newValues = values as string[];
|
||||
setSelectedOwnerUrns(newValues);
|
||||
|
||||
// Find new owner URNs that weren't previously selected
|
||||
const newOwnerUrns = newValues.filter((urn) => !pendingOwners.some((owner) => owner.ownerUrn === urn));
|
||||
|
||||
if (newOwnerUrns.length > 0 && defaultOwnerType) {
|
||||
const newPendingOwners = newOwnerUrns.map((urn) => {
|
||||
const foundEntity = searchResults.find((e) => e.urn === urn);
|
||||
const ownerEntityType =
|
||||
foundEntity && foundEntity.type === EntityType.CorpGroup
|
||||
? OwnerEntityType.CorpGroup
|
||||
: OwnerEntityType.CorpUser;
|
||||
|
||||
return {
|
||||
ownerUrn: urn,
|
||||
ownerEntityType,
|
||||
ownershipTypeUrn: defaultOwnerType,
|
||||
};
|
||||
});
|
||||
|
||||
setPendingOwners([...pendingOwners, ...newPendingOwners]);
|
||||
}
|
||||
|
||||
// Handle removed owners
|
||||
if (newValues.length < selectedOwnerUrns.length) {
|
||||
const removedUrns = selectedOwnerUrns.filter((urn) => !newValues.includes(urn));
|
||||
const updatedPendingOwners = pendingOwners.filter((owner) => !removedUrns.includes(owner.ownerUrn));
|
||||
setPendingOwners(updatedPendingOwners);
|
||||
}
|
||||
};
|
||||
|
||||
// Loading state for the select
|
||||
const isSelectLoading = isSearching && searchLoading;
|
||||
|
||||
// Simplified conditional content for notFoundContent
|
||||
let notFoundContent: React.ReactNode = null;
|
||||
if (isSelectLoading) {
|
||||
notFoundContent = 'Loading...';
|
||||
} else if (inputValue && searchResults.length === 0) {
|
||||
notFoundContent = 'No results found';
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<SectionHeader>
|
||||
<Text>Add Owners</Text>
|
||||
</SectionHeader>
|
||||
<FormSection>
|
||||
<SelectInput
|
||||
mode="multiple"
|
||||
placeholder="Search for users or groups"
|
||||
showSearch
|
||||
filterOption={false}
|
||||
onSearch={handleOwnerSearch}
|
||||
onChange={handleSelectChange}
|
||||
value={selectedOwnerUrns}
|
||||
loading={isSelectLoading}
|
||||
notFoundContent={notFoundContent}
|
||||
optionLabelProp="label"
|
||||
>
|
||||
{searchResults.map((entity) => renderSearchResult(entity))}
|
||||
</SelectInput>
|
||||
</FormSection>
|
||||
</SectionContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OwnersSection;
|
||||
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Input, ColorPicker } from '@components';
|
||||
|
||||
// Tag details section props
|
||||
export interface TagDetailsProps {
|
||||
tagName: string;
|
||||
setTagName: React.Dispatch<React.SetStateAction<string>>;
|
||||
tagDescription: string;
|
||||
setTagDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||
tagColor: string;
|
||||
setTagColor: (color: string) => void;
|
||||
}
|
||||
|
||||
const SectionContainer = styled.div`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const FormSection = styled.div`
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Component for tag name, description, and color selection
|
||||
*/
|
||||
const TagDetailsSection: React.FC<TagDetailsProps> = ({
|
||||
tagName,
|
||||
setTagName,
|
||||
tagDescription,
|
||||
setTagDescription,
|
||||
tagColor,
|
||||
setTagColor,
|
||||
}) => {
|
||||
const handleTagNameChange: React.Dispatch<React.SetStateAction<string>> = (value) => {
|
||||
if (typeof value === 'function') {
|
||||
setTagName(value);
|
||||
} else {
|
||||
setTagName(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDescriptionChange: React.Dispatch<React.SetStateAction<string>> = (value) => {
|
||||
if (typeof value === 'function') {
|
||||
setTagDescription(value);
|
||||
} else {
|
||||
setTagDescription(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
setTagColor(color);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<FormSection>
|
||||
<Input
|
||||
label="Name"
|
||||
value={tagName}
|
||||
setValue={handleTagNameChange}
|
||||
placeholder="Enter tag name"
|
||||
required
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<Input
|
||||
label="Description"
|
||||
value={tagDescription}
|
||||
setValue={handleDescriptionChange}
|
||||
placeholder="Add a description for your new tag"
|
||||
type="textarea"
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<ColorPicker initialColor={tagColor} onChange={handleColorChange} label="Color" />
|
||||
</FormSection>
|
||||
</SectionContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagDetailsSection;
|
||||
22
datahub-web-react/src/app/tags/CreateNewTagModal/types.ts
Normal file
22
datahub-web-react/src/app/tags/CreateNewTagModal/types.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// Interface for modal buttons matching the expected ButtonProps
|
||||
export interface ModalButton {
|
||||
text: string;
|
||||
color: 'violet' | 'white' | 'black' | 'green' | 'red' | 'blue' | 'yellow' | 'gray';
|
||||
variant: 'text' | 'filled' | 'outline';
|
||||
onClick: () => void;
|
||||
id?: string;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
// Common styled components
|
||||
export const FormSection = {
|
||||
marginBottom: '16px',
|
||||
};
|
||||
|
||||
// Create tag result interface
|
||||
export interface CreateTagResult {
|
||||
tagUrn: string;
|
||||
success: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
@ -446,6 +446,7 @@ const ManageTag = ({ tagUrn, onClose, onSave, isModalOpen = false }: Props) => {
|
||||
entityUrn={tagUrn}
|
||||
owner={ownerItem}
|
||||
hidePopOver
|
||||
refetch={refetch}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useShowNavBarRedesign } from '@src/app/useShowNavBarRedesign';
|
||||
import { SearchBar, PageTitle, Pagination } from '@components';
|
||||
import { SearchBar, PageTitle, Pagination, Button, Tooltip2 } from '@components';
|
||||
import { useGetSearchResultsForMultipleQuery } from '@src/graphql/search.generated';
|
||||
import { EntityType } from '@src/types.generated';
|
||||
import { Message } from '@src/app/shared/Message';
|
||||
@ -9,6 +9,8 @@ import styled from 'styled-components';
|
||||
import { PageContainer } from '../govern/structuredProperties/styledComponents';
|
||||
import EmptyTags from './EmptyTags';
|
||||
import TagsTable from './TagsTable';
|
||||
import CreateNewTagModal from './CreateNewTagModal/CreateNewTagModal';
|
||||
import { useUserContext } from '../context/useUserContext';
|
||||
|
||||
const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
@ -55,6 +57,11 @@ const ManageTags = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('*');
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const [showCreateTagModal, setShowCreateTagModal] = useState(false);
|
||||
|
||||
// Check permissions using UserContext
|
||||
const userContext = useUserContext();
|
||||
const canCreateTags = userContext?.platformPrivileges?.createTags || userContext?.platformPrivileges?.manageTags;
|
||||
|
||||
// Debounce search query input to reduce unnecessary renders
|
||||
useEffect(() => {
|
||||
@ -110,12 +117,45 @@ const ManageTags = () => {
|
||||
return <Message type="error" content={`Failed to load tags: ${searchError.message}`} />;
|
||||
}
|
||||
|
||||
// Create the Create Tag button with proper permissions handling
|
||||
const renderCreateTagButton = () => {
|
||||
if (!canCreateTags) {
|
||||
return (
|
||||
<Tooltip2
|
||||
title="You do not have permission to create tags"
|
||||
placement="left"
|
||||
showArrow
|
||||
mouseEnterDelay={0.1}
|
||||
mouseLeaveDelay={0.1}
|
||||
>
|
||||
<span>
|
||||
<Button size="md" color="violet" icon={{ icon: 'Plus', source: 'phosphor' }} disabled>
|
||||
Create Tag
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip2>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => setShowCreateTagModal(true)}
|
||||
size="md"
|
||||
color="violet"
|
||||
icon={{ icon: 'Plus', source: 'phosphor' }}
|
||||
>
|
||||
Create Tag
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer $isShowNavBarRedesign={isShowNavBarRedesign}>
|
||||
{searchLoading && <LoadingBar />}
|
||||
|
||||
<HeaderContainer>
|
||||
<PageTitle title="Manage Tags" subTitle="Create and edit asset & column tags" />
|
||||
{renderCreateTagButton()}
|
||||
</HeaderContainer>
|
||||
|
||||
<SearchContainer>
|
||||
@ -148,6 +188,14 @@ const ManageTags = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CreateNewTagModal
|
||||
open={showCreateTagModal}
|
||||
onClose={() => {
|
||||
setShowCreateTagModal(false);
|
||||
setTimeout(() => refetch(), 3000);
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { NetworkStatus } from '@apollo/client';
|
||||
import { Table } from '@components';
|
||||
import { Table, Modal } from '@components';
|
||||
import { message } from 'antd';
|
||||
import { AlignmentOptions } from '@src/alchemy-components/theme/config';
|
||||
import { useEntityRegistry } from '@src/app/useEntityRegistry';
|
||||
import { GetSearchResultsForMultipleQuery } from '@src/graphql/search.generated';
|
||||
import { EntityType } from '@src/types.generated';
|
||||
import { useDeleteTagMutation } from '../../graphql/tag.generated';
|
||||
import { useUserContext } from '../context/useUserContext';
|
||||
import { ManageTag } from './ManageTag';
|
||||
import {
|
||||
TagActionsColumn,
|
||||
@ -25,6 +28,11 @@ interface Props {
|
||||
|
||||
const TagsTable = ({ searchQuery, searchData, loading: propLoading, networkStatus, refetch }: Props) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const userContext = useUserContext();
|
||||
const [deleteTagMutation] = useDeleteTagMutation();
|
||||
|
||||
// Check if user has permission to manage or delete tags
|
||||
const canManageTags = Boolean(userContext?.platformPrivileges?.manageTags);
|
||||
|
||||
// Optimize the tagsData with useMemo to prevent unnecessary filtering on re-renders
|
||||
const tagsData = useMemo(() => {
|
||||
@ -34,6 +42,11 @@ const TagsTable = ({ searchQuery, searchData, loading: propLoading, networkStatu
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [editingTag, setEditingTag] = useState('');
|
||||
|
||||
// Simplified state for delete confirmation modal
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [tagUrnToDelete, setTagUrnToDelete] = useState('');
|
||||
const [tagDisplayName, setTagDisplayName] = useState('');
|
||||
|
||||
const [sortedInfo, setSortedInfo] = useState<{
|
||||
columnKey?: string;
|
||||
order?: 'ascend' | 'descend';
|
||||
@ -62,6 +75,43 @@ const TagsTable = ({ searchQuery, searchData, loading: propLoading, networkStatu
|
||||
|
||||
const isLoading = propLoading || networkStatus === NetworkStatus.refetch;
|
||||
|
||||
// Simplified function to initiate tag deletion
|
||||
const showDeleteConfirmation = useCallback(
|
||||
(tagUrn: string) => {
|
||||
// Find the tag entity from tagsData
|
||||
const tagData = tagsData.find((result) => result.entity.urn === tagUrn);
|
||||
if (!tagData) {
|
||||
message.error('Failed to find tag information');
|
||||
return;
|
||||
}
|
||||
|
||||
const fullDisplayName = entityRegistry.getDisplayName(EntityType.Tag, tagData.entity);
|
||||
|
||||
setTagUrnToDelete(tagUrn);
|
||||
setTagDisplayName(fullDisplayName);
|
||||
setShowDeleteModal(true);
|
||||
},
|
||||
[entityRegistry, tagsData],
|
||||
);
|
||||
|
||||
// Function to handle the actual tag deletion
|
||||
const handleDeleteTag = useCallback(() => {
|
||||
deleteTagMutation({
|
||||
variables: {
|
||||
urn: tagUrnToDelete,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
message.success(`Tag "${tagDisplayName}" has been deleted`);
|
||||
refetch(); // Refresh the tag list
|
||||
})
|
||||
.catch((e: any) => {
|
||||
message.error(`Failed to delete tag: ${e.message}`);
|
||||
});
|
||||
|
||||
setShowDeleteModal(false);
|
||||
}, [deleteTagMutation, refetch, tagUrnToDelete, tagDisplayName]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
@ -119,12 +169,20 @@ const TagsTable = ({ searchQuery, searchData, loading: propLoading, networkStatu
|
||||
setEditingTag(record.entity.urn);
|
||||
setShowEdit(true);
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (canManageTags) {
|
||||
showDeleteConfirmation(record.entity.urn);
|
||||
} else {
|
||||
message.error('You do not have permission to delete tags');
|
||||
}
|
||||
}}
|
||||
canManageTags={canManageTags}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[entityRegistry, searchQuery, sortedInfo],
|
||||
[entityRegistry, searchQuery, sortedInfo, canManageTags, showDeleteConfirmation],
|
||||
);
|
||||
|
||||
// Generate table data once with memoization
|
||||
@ -154,6 +212,30 @@ const TagsTable = ({ searchQuery, searchData, loading: propLoading, networkStatu
|
||||
isModalOpen={showEdit}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete confirmation modal - simplified */}
|
||||
<Modal
|
||||
title={`Delete tag ${tagDisplayName}`}
|
||||
onCancel={() => setShowDeleteModal(false)}
|
||||
open={showDeleteModal}
|
||||
centered
|
||||
buttons={[
|
||||
{
|
||||
text: 'Cancel',
|
||||
color: 'violet',
|
||||
variant: 'text',
|
||||
onClick: () => setShowDeleteModal(false),
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
color: 'red',
|
||||
variant: 'filled',
|
||||
onClick: handleDeleteTag,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>Are you sure you want to delete this tag? This action cannot be undone.</p>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -99,7 +99,11 @@ export const TagDescriptionColumn = React.memo(({ tagUrn }: { tagUrn: string })
|
||||
});
|
||||
|
||||
export const TagOwnersColumn = React.memo(({ tagUrn }: { tagUrn: string }) => {
|
||||
const { data, loading: ownerLoading } = useGetTagQuery({
|
||||
const {
|
||||
data,
|
||||
loading: ownerLoading,
|
||||
refetch: refetchOwners,
|
||||
} = useGetTagQuery({
|
||||
variables: { urn: tagUrn },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
@ -115,7 +119,13 @@ export const TagOwnersColumn = React.memo(({ tagUrn }: { tagUrn: string }) => {
|
||||
<ColumnContainer>
|
||||
<OwnersContainer>
|
||||
{ownershipData.map((ownerItem) => (
|
||||
<ExpandedOwner key={ownerItem.owner?.urn} entityUrn={tagUrn} owner={ownerItem as any} hidePopOver />
|
||||
<ExpandedOwner
|
||||
key={ownerItem.owner?.urn}
|
||||
entityUrn={tagUrn}
|
||||
owner={ownerItem as any}
|
||||
hidePopOver
|
||||
refetch={refetchOwners}
|
||||
/>
|
||||
))}
|
||||
</OwnersContainer>
|
||||
</ColumnContainer>
|
||||
@ -225,26 +235,54 @@ export const TagAppliedToColumn = React.memo(({ tagUrn }: { tagUrn: string }) =>
|
||||
);
|
||||
});
|
||||
|
||||
export const TagActionsColumn = React.memo(({ tagUrn, onEdit }: { tagUrn: string; onEdit: () => void }) => {
|
||||
const items = [
|
||||
{
|
||||
key: '0',
|
||||
label: (
|
||||
<MenuItem onClick={onEdit} data-testid="action-edit">
|
||||
Edit
|
||||
</MenuItem>
|
||||
),
|
||||
},
|
||||
];
|
||||
export const TagActionsColumn = React.memo(
|
||||
({
|
||||
tagUrn,
|
||||
onEdit,
|
||||
onDelete,
|
||||
canManageTags,
|
||||
}: {
|
||||
tagUrn: string;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
canManageTags: boolean;
|
||||
}) => {
|
||||
const items = [
|
||||
{
|
||||
key: '0',
|
||||
label: (
|
||||
<MenuItem onClick={onEdit} data-testid="action-edit">
|
||||
Edit
|
||||
</MenuItem>
|
||||
),
|
||||
},
|
||||
...(canManageTags
|
||||
? [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<MenuItem
|
||||
onClick={onDelete}
|
||||
data-testid="action-delete"
|
||||
style={{ color: colors.red[500] }}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<CardIcons>
|
||||
<Dropdown menu={{ items }} trigger={['click']} data-testid={`${tagUrn}-actions-dropdown`}>
|
||||
<Icon icon="MoreVert" size="md" />
|
||||
</Dropdown>
|
||||
</CardIcons>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<CardIcons>
|
||||
<Dropdown menu={{ items }} trigger={['click']} data-testid={`${tagUrn}-actions-dropdown`}>
|
||||
<Icon icon="MoreVert" size="md" />
|
||||
</Dropdown>
|
||||
</CardIcons>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const TagColorColumn = React.memo(({ tag }: { tag: Entity }) => {
|
||||
const colorHex = getTagColor(tag);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user