fix(ui/navBar): Fix logic to display manage tags link (#13564)

Co-authored-by: v-tarasevich-blitz-brain <v.tarasevich@blitz-brain.com>
This commit is contained in:
Andrew Sikowitz 2025-07-03 13:53:02 -07:00 committed by GitHub
parent ae234d671e
commit 416c2093d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 258 additions and 29 deletions

View File

@ -50,7 +50,9 @@ const ButtonsContainer = styled.div`
export interface ModalButton extends ButtonProps { export interface ModalButton extends ButtonProps {
text: string; text: string;
key?: string;
onClick: () => void; onClick: () => void;
buttonDataTestId?: string;
} }
export interface ModalProps { export interface ModalProps {
@ -78,6 +80,7 @@ export function Modal({
onCancel={onCancel} onCancel={onCancel}
closeIcon={<Icon icon="X" source="phosphor" />} closeIcon={<Icon icon="X" source="phosphor" />}
hasChildren={!!children} hasChildren={!!children}
data-testid={dataTestId}
title={ title={
<HeaderContainer hasChildren={!!children}> <HeaderContainer hasChildren={!!children}>
<Heading type="h1" color="gray" colorLevel={600} weight="bold" size="lg"> <Heading type="h1" color="gray" colorLevel={600} weight="bold" size="lg">
@ -93,10 +96,10 @@ export function Modal({
footer={ footer={
!!buttons.length && ( !!buttons.length && (
<ButtonsContainer> <ButtonsContainer>
{buttons.map(({ text, variant, onClick, ...buttonProps }, index) => ( {buttons.map(({ text, variant, onClick, key, buttonDataTestId, ...buttonProps }, index) => (
<Button <Button
key={text} key={key || text}
data-testid={dataTestId && `${dataTestId}-${variant}-${index}`} data-testid={buttonDataTestId ?? (dataTestId && `${dataTestId}-${variant}-${index}`)}
variant={variant} variant={variant}
onClick={onClick} onClick={onClick}
{...buttonProps} {...buttonProps}

View File

@ -94,6 +94,9 @@ export const NavSidebar = () => {
const showStructuredProperties = const showStructuredProperties =
config?.featureFlags?.showManageStructuredProperties && config?.featureFlags?.showManageStructuredProperties &&
(me.platformPrivileges?.manageStructuredProperties || me.platformPrivileges?.viewStructuredPropertiesPage); (me.platformPrivileges?.manageStructuredProperties || me.platformPrivileges?.viewStructuredPropertiesPage);
const showManageTags =
config?.featureFlags?.showManageTags &&
(me.platformPrivileges?.manageTags || me.platformPrivileges?.viewManageTags);
const showDataSources = const showDataSources =
config.managedIngestionConfig.enabled && config.managedIngestionConfig.enabled &&
@ -151,6 +154,7 @@ export const NavSidebar = () => {
icon: <Tag />, icon: <Tag />,
selectedIcon: <Tag weight="fill" />, selectedIcon: <Tag weight="fill" />,
link: PageRoutes.MANAGE_TAGS, link: PageRoutes.MANAGE_TAGS,
isHidden: !showManageTags,
}, },
{ {
type: NavBarMenuItemTypes.Item, type: NavBarMenuItemTypes.Item,

View File

@ -66,7 +66,7 @@ export const NoPageFound = () => {
<Number>4</Number> <Number>4</Number>
</NumberContainer> </NumberContainer>
</PageNotFoundTextContainer> </PageNotFoundTextContainer>
<SubTitle>The page your requested was not found,</SubTitle> <SubTitle>The page you requested was not found,</SubTitle>
<Button onClick={goToHomepage}>Back to Home</Button> <Button onClick={goToHomepage}>Back to Home</Button>
</PageNotFoundContainer> </PageNotFoundContainer>
</MainContainer> </MainContainer>

View File

@ -2,10 +2,11 @@ import { Modal } from '@components';
import { message } from 'antd'; import { message } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ModalButton } from '@components/components/Modal/Modal';
import { useEnterKeyListener } from '@app/shared/useEnterKeyListener'; import { useEnterKeyListener } from '@app/shared/useEnterKeyListener';
import OwnersSection, { PendingOwner } from '@app/sharedV2/owners/OwnersSection'; import OwnersSection, { PendingOwner } from '@app/sharedV2/owners/OwnersSection';
import TagDetailsSection from '@app/tags/CreateNewTagModal/TagDetailsSection'; import TagDetailsSection from '@app/tags/CreateNewTagModal/TagDetailsSection';
import { ModalButton } from '@app/tags/CreateNewTagModal/types';
import { useBatchAddOwnersMutation, useSetTagColorMutation } from '@graphql/mutations.generated'; import { useBatchAddOwnersMutation, useSetTagColorMutation } from '@graphql/mutations.generated';
import { useCreateTagMutation } from '@graphql/tag.generated'; import { useCreateTagMutation } from '@graphql/tag.generated';
@ -120,6 +121,7 @@ const CreateNewTagModal: React.FC<CreateNewTagModalProps> = ({ onClose, open })
color: 'violet', color: 'violet',
variant: 'text', variant: 'text',
onClick: onClose, onClick: onClose,
buttonDataTestId: 'create-tag-modal-cancel-button',
}, },
{ {
text: 'Create', text: 'Create',
@ -129,6 +131,7 @@ const CreateNewTagModal: React.FC<CreateNewTagModalProps> = ({ onClose, open })
onClick: onOk, onClick: onOk,
disabled: !tagName || isLoading, disabled: !tagName || isLoading,
isLoading, isLoading,
buttonDataTestId: 'create-tag-modal-create-button',
}, },
]; ];

View File

@ -59,6 +59,7 @@ const TagDetailsSection: React.FC<TagDetailsProps> = ({
value={tagName} value={tagName}
setValue={handleTagNameChange} setValue={handleTagNameChange}
placeholder="Enter tag name" placeholder="Enter tag name"
data-testid="tag-name-field"
required required
/> />
</FormSection> </FormSection>
@ -69,6 +70,7 @@ const TagDetailsSection: React.FC<TagDetailsProps> = ({
value={tagDescription} value={tagDescription}
setValue={handleDescriptionChange} setValue={handleDescriptionChange}
placeholder="Add a description for your new tag" placeholder="Add a description for your new tag"
data-testid="tag-description-field"
type="textarea" type="textarea"
/> />
</FormSection> </FormSection>

View File

@ -1,14 +1,3 @@
// 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 // Common styled components
export const FormSection = { export const FormSection = {
marginBottom: '16px', marginBottom: '16px',

View File

@ -1,8 +1,10 @@
import { ButtonProps, ColorPicker, Input, Modal } from '@components'; import { ColorPicker, Input, Modal } from '@components';
import { message } from 'antd'; import { message } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ModalButton } from '@components/components/Modal/Modal';
import OwnersSection from '@app/sharedV2/owners/OwnersSection'; import OwnersSection from '@app/sharedV2/owners/OwnersSection';
import { useEntityRegistry } from '@src/app/useEntityRegistry'; import { useEntityRegistry } from '@src/app/useEntityRegistry';
import { import {
@ -24,12 +26,6 @@ interface Props {
isModalOpen?: boolean; isModalOpen?: boolean;
} }
// Define a compatible interface for modal buttons
interface ModalButton extends ButtonProps {
text: string;
onClick: () => void;
}
// Interface for pending owner // Interface for pending owner
interface PendingOwner { interface PendingOwner {
ownerUrn: string; ownerUrn: string;
@ -239,6 +235,7 @@ const ManageTag = ({ tagUrn, onClose, onSave, isModalOpen = false }: Props) => {
variant: 'filled', variant: 'filled',
onClick: handleSave, onClick: handleSave,
disabled: !hasChanges(), disabled: !hasChanges(),
buttonDataTestId: 'update-tag-button',
}, },
]; ];
@ -251,7 +248,15 @@ const ManageTag = ({ tagUrn, onClose, onSave, isModalOpen = false }: Props) => {
const modalTitle = tagDisplayName ? `Edit Tag: ${tagDisplayName}` : 'Edit Tag'; const modalTitle = tagDisplayName ? `Edit Tag: ${tagDisplayName}` : 'Edit Tag';
return ( return (
<Modal title={modalTitle} onCancel={onClose} buttons={buttons} open={isModalOpen} centered width={400}> <Modal
title={modalTitle}
onCancel={onClose}
buttons={buttons}
open={isModalOpen}
centered
width={400}
dataTestId="edit-tag-modal"
>
<div> <div>
<FormSection> <FormSection>
<Input <Input
@ -260,6 +265,7 @@ const ManageTag = ({ tagUrn, onClose, onSave, isModalOpen = false }: Props) => {
setValue={handleDescriptionChange} setValue={handleDescriptionChange}
placeholder="Tag description" placeholder="Tag description"
type="textarea" type="textarea"
data-testid="tag-description-field"
/> />
</FormSection> </FormSection>

View File

@ -145,6 +145,7 @@ const ManageTags = () => {
size="md" size="md"
color="violet" color="violet"
icon={{ icon: 'Plus', source: 'phosphor' }} icon={{ icon: 'Plus', source: 'phosphor' }}
data-testid="add-tag-button"
> >
Create Tag Create Tag
</Button> </Button>

View File

@ -233,6 +233,7 @@ const TagsTable = ({ searchQuery, searchData, loading: propLoading, networkStatu
color: 'red', color: 'red',
variant: 'filled', variant: 'filled',
onClick: handleDeleteTag, onClick: handleDeleteTag,
buttonDataTestId: 'delete-tag-button',
}, },
]} ]}
> >

View File

@ -290,7 +290,7 @@ export const TagActionsColumn = React.memo(
return ( return (
<CardIcons> <CardIcons>
<Dropdown menu={{ items }} trigger={['click']} data-testid={`${tagUrn}-actions-dropdown`}> <Dropdown menu={{ items }} trigger={['click']} data-testid={`${tagUrn}-actions-dropdown`}>
<Icon icon="MoreVert" size="md" /> <Icon icon="MoreVert" size="md" data-testid={`${tagUrn}-actions`} />
</Dropdown> </Dropdown>
</CardIcons> </CardIcons>
); );

View File

@ -1,6 +1,10 @@
describe("manage tags", () => { describe("manage tags", () => {
it("Manage Tags Page - Verify search bar placeholder", () => { beforeEach(() => {
cy.setIsThemeV2Enabled(false);
cy.login(); cy.login();
});
it("Manage Tags Page - Verify search bar placeholder", () => {
cy.visit("/tags"); cy.visit("/tags");
cy.get('[data-testid="tag-search-input"]').should( cy.get('[data-testid="tag-search-input"]').should(
"have.attr", "have.attr",
@ -8,8 +12,8 @@ describe("manage tags", () => {
"Search tags...", "Search tags...",
); );
}); });
it("Manage Tags Page - Verify Title, Search, and Results", () => { it("Manage Tags Page - Verify Title, Search, and Results", () => {
cy.login();
cy.visit("/tags"); cy.visit("/tags");
cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags"); cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags");
cy.get('[data-testid="urn:li:tag:Cypress-name"]').should( cy.get('[data-testid="urn:li:tag:Cypress-name"]').should(
@ -22,15 +26,15 @@ describe("manage tags", () => {
"Cypress", "Cypress",
); );
}); });
it("Manage Tags Page - Verify search not exists", () => { it("Manage Tags Page - Verify search not exists", () => {
cy.login();
cy.visit("/tags"); cy.visit("/tags");
cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags"); cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags");
cy.get('[data-testid="urn:li:tag:Cypress-name"]').should( cy.get('[data-testid="urn:li:tag:Cypress-name"]').should(
"contain.text", "contain.text",
"Cypress", "Cypress",
); );
cy.get('[data-testid="tag-search-input"]').type("test"); cy.get('[data-testid="tag-search-input"]').type("invalidvalue");
cy.get('[data-testid="tags-not-found"]').should( cy.get('[data-testid="tags-not-found"]').should(
"contain.text", "contain.text",
"No tags found for your search query", "No tags found for your search query",

View File

@ -0,0 +1,53 @@
export default class DatasetHelper {
static openDataset(urn, name) {
cy.goToDataset(urn, name);
}
static assignTag(name) {
cy.get("#entity-profile-tags").within(() => {
cy.clickOptionWithTestId("AddRoundedIcon");
});
cy.getWithTestId("tag-term-modal-input").within(() => {
cy.get("input").focus({ force: true }).type(name);
});
cy.get(`[name="${name}"]`).click();
cy.clickOptionWithTestId("add-tag-term-from-modal-btn");
cy.waitTextVisible("Added Tags!");
}
static ensureTagIsAssigned(name) {
cy.getWithTestId(`tag-${name}`).should("be.visible");
}
static unassignTag(name) {
cy.getWithTestId(`tag-${name}`).within(() => {
cy.get(".ant-tag-close-icon").click();
});
cy.get(".ant-modal-confirm-confirm").within(() => {
cy.get(".ant-btn-primary").click();
});
cy.waitTextVisible("Removed Tag!");
}
static ensureTagIsNotAssigned(name) {
cy.getWithTestId(`tag-${name}`).should("not.exist");
}
static searchByTag(tagName) {
cy.visit(
`/search?filter_tags___false___EQUAL___0=urn%3Ali%3Atag%3A${tagName}&page=1&query=%2A&unionType=0`,
);
}
static ensureEntityIsInSearchResults(urn) {
cy.getWithTestId(`preview-${urn}`).should("be.visible");
}
static ensureEntityIsNotInSearchResults(urn) {
cy.getWithTestId(`preview-${urn}`).should("not.exist");
}
}

View File

@ -0,0 +1,71 @@
export default class TagsPageHelper {
static openPage() {
cy.visit("/tags");
}
static getTagUrn(name) {
return `urn:li:tag:${name}`;
}
static create(name, description, shouldBeSuccessfullyCreated = true) {
cy.clickOptionWithTestId("add-tag-button");
cy.getWithTestId("tag-name-field").within(() =>
cy.get("input").focus().type(name),
);
cy.getWithTestId("tag-description-field").within(() =>
cy.get("input").focus().type(description),
);
cy.clickOptionWithTestId("create-tag-modal-create-button");
if (shouldBeSuccessfullyCreated) {
cy.waitTextVisible(`Tag "${name}" successfully created`);
} else {
cy.waitTextVisible("Failed to create tag. An unexpected error occurred");
cy.clickOptionWithTestId("create-tag-modal-cancel-button");
}
}
static remove(name) {
cy.getWithTestId("tag-search-input").focus().type(name, { delay: 0 });
cy.clickOptionWithTestId(`${TagsPageHelper.getTagUrn(name)}-actions`);
cy.clickOptionWithTestId("action-delete");
cy.clickOptionWithTestId("delete-tag-button");
cy.getWithTestId("tag-search-input").clear();
}
static edit(name, newDescription) {
cy.getWithTestId("tag-search-input").focus().type(name, { delay: 0 });
cy.clickOptionWithTestId(`${TagsPageHelper.getTagUrn(name)}-actions`);
cy.clickOptionWithTestId("action-edit");
cy.getWithTestId("edit-tag-modal").within(() => {
cy.getWithTestId("tag-description-field").within(() =>
cy.get("input").focus().clear().type(newDescription),
);
});
cy.clickOptionWithTestId("update-tag-button");
cy.getWithTestId("tag-search-input").clear();
}
static ensureTagIsInTable(name, description) {
cy.getWithTestId("tag-search-input").focus().type(name, { delay: 0 });
cy.getWithTestId(`${TagsPageHelper.getTagUrn(name)}-name`).should(
"contain",
name,
);
cy.getWithTestId(`${TagsPageHelper.getTagUrn(name)}-description`).should(
"contain",
description,
);
cy.getWithTestId("tag-search-input").clear();
}
static ensureTagIsNotInTable(name) {
cy.getWithTestId("tag-search-input").focus().type(name, { delay: 0 });
cy.getWithTestId(`${TagsPageHelper.getTagUrn(name)}-name`).should(
"not.exist",
);
cy.getWithTestId("tag-search-input").clear();
}
}

View File

@ -0,0 +1,92 @@
import DatasetHelper from "./helpers/dataset_helper";
import TagsPageHelper from "./helpers/tags_page_helper";
const test_id = `manage_tagsV2_${new Date().getTime()}`;
const SAMPLE_DATASET_URN =
"urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD)";
const SAMPLE_DATASET_NAME = "SampleCypressHiveDataset";
describe("tags", () => {
beforeEach(() => {
cy.setIsThemeV2Enabled(true);
cy.login();
});
it("verify search bar placeholder", () => {
cy.visit("/tags");
cy.get('[data-testid="tag-search-input"]').should(
"have.attr",
"placeholder",
"Search tags...",
);
});
it("verify title, search, and results", () => {
cy.visit("/tags");
cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags");
cy.get('[data-testid="urn:li:tag:Cypress-name"]').should(
"contain.text",
"Cypress",
);
cy.get('[data-testid="tag-search-input"]').type("Cypress");
cy.get('[data-testid="urn:li:tag:Cypress-name"]').should(
"contain.text",
"Cypress",
);
});
it("verify search not exists", () => {
cy.visit("/tags");
cy.get('[data-testid="page-title"]').should("contain.text", "Manage Tags");
cy.get('[data-testid="urn:li:tag:Cypress-name"]').should(
"contain.text",
"Cypress",
);
cy.get('[data-testid="tag-search-input"]').type("invalidvalue");
cy.get('[data-testid="tags-not-found"]').should(
"contain.text",
"No tags found for your search query",
);
});
it("should allow to create/edit/remove tags on tags page", () => {
const tagName = `tag_${test_id}_tags_page`;
const tagDescription = `${tagName} description`;
TagsPageHelper.openPage();
TagsPageHelper.create(tagName, tagDescription);
TagsPageHelper.ensureTagIsInTable(tagName, tagDescription);
// ensure that we can't to create tag with the same name
TagsPageHelper.create(tagName, tagDescription, false);
TagsPageHelper.edit(tagName, `${tagDescription} edited`);
TagsPageHelper.ensureTagIsInTable(tagName, `${tagDescription} edited`);
TagsPageHelper.remove(tagName);
TagsPageHelper.ensureTagIsNotInTable(tagName);
});
it("should allow to assign/unassign tags on a dataset", () => {
const tagName = `tag_${test_id}_dataset`;
const tagDescription = `${tagName} description`;
TagsPageHelper.openPage();
TagsPageHelper.create(tagName, tagDescription);
DatasetHelper.openDataset(SAMPLE_DATASET_URN, SAMPLE_DATASET_NAME);
DatasetHelper.assignTag(tagName);
DatasetHelper.ensureTagIsAssigned(tagName);
DatasetHelper.searchByTag(tagName);
DatasetHelper.ensureEntityIsInSearchResults(SAMPLE_DATASET_URN);
DatasetHelper.openDataset(SAMPLE_DATASET_URN, SAMPLE_DATASET_NAME);
DatasetHelper.unassignTag(tagName);
DatasetHelper.ensureTagIsNotAssigned(tagName);
DatasetHelper.searchByTag(tagName);
DatasetHelper.ensureEntityIsNotInSearchResults(SAMPLE_DATASET_URN);
TagsPageHelper.openPage();
TagsPageHelper.remove(tagName);
});
});