diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AsyncSelectList/AsyncSelectList.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/AsyncSelectList/AsyncSelectList.interface.ts index 0641d535833..b81456b4db3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AsyncSelectList/AsyncSelectList.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/AsyncSelectList/AsyncSelectList.interface.ts @@ -24,9 +24,11 @@ export type SelectOption = { export interface AsyncSelectListProps { mode?: 'multiple'; + className?: string; placeholder?: string; debounceTimeout?: number; defaultValue?: string[]; + initialData?: SelectOption[]; onChange?: (option: DefaultOptionType | DefaultOptionType[]) => void; fetchOptions: ( search: string, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AsyncSelectList/AsyncSelectList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AsyncSelectList/AsyncSelectList.tsx index d0c0e5d1899..8a5847d46a5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AsyncSelectList/AsyncSelectList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AsyncSelectList/AsyncSelectList.tsx @@ -10,17 +10,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Select, SelectProps, Space, Tooltip, Typography } from 'antd'; -import { DefaultOptionType } from 'antd/lib/select'; +import { CloseOutlined } from '@ant-design/icons'; +import { + Select, + SelectProps, + Space, + TagProps, + Tooltip, + Typography, +} from 'antd'; import { AxiosError } from 'axios'; -import { debounce, isEmpty } from 'lodash'; -import React, { FC, useCallback, useMemo, useState } from 'react'; +import { debounce, isEmpty, isUndefined } from 'lodash'; +import { CustomTagProps } from 'rc-select/lib/BaseSelect'; +import React, { FC, useCallback, useMemo, useRef, useState } from 'react'; import Loader from '../../components/Loader/Loader'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; +import { TAG_START_WITH } from '../../constants/Tag.constants'; import { Paging } from '../../generated/type/paging'; +import { TagLabel } from '../../generated/type/tagLabel'; import Fqn from '../../utils/Fqn'; -import { tagRender } from '../../utils/TagsUtils'; +import { getTagDisplay, tagRender } from '../../utils/TagsUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import TagsV1 from '../Tag/TagsV1/TagsV1.component'; import { AsyncSelectListProps, SelectOption, @@ -31,6 +42,8 @@ const AsyncSelectList: FC = ({ onChange, fetchOptions, debounceTimeout = 800, + initialData, + className, ...props }) => { const [isLoading, setIsLoading] = useState(false); @@ -39,6 +52,7 @@ const AsyncSelectList: FC = ({ const [searchValue, setSearchValue] = useState(''); const [paging, setPaging] = useState({} as Paging); const [currentPage, setCurrentPage] = useState(1); + const selectedTagsRef = useRef(initialData ?? []); const loadOptions = useCallback( async (value: string) => { @@ -84,7 +98,11 @@ const AsyncSelectList: FC = ({ className="text-grey-muted m-0 p-0"> {parts.join(FQN_SEPARATOR_CHAR)} - {lastPartOfTag} + + {lastPartOfTag} + ), value: tag.value, @@ -124,15 +142,56 @@ const AsyncSelectList: FC = ({ ); + const customTagRender = (data: CustomTagProps) => { + const selectedTag = selectedTagsRef.current.find( + (tag) => tag.value === data.label + ); + + if (isUndefined(selectedTag?.data)) { + return tagRender(data); + } + + const { label, onClose } = data; + const tagLabel = getTagDisplay(label as string); + + const onPreventMouseDown = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + const tagProps = { + closable: true, + closeIcon: ( + + ), + 'data-testid': `selected-tag-${tagLabel}`, + onClose, + onMouseDown: onPreventMouseDown, + } as TagProps; + + return ( + + ); + }; + const handleChange: SelectProps['onChange'] = (values: string[], options) => { const selectedValues = values.map((value) => { - const data = (options as DefaultOptionType[]).find( + const data = (options as SelectOption[]).find( (option) => option.value === value ); return data ?? { value, label: value }; }); - + selectedTagsRef.current = selectedValues; onChange?.(selectedValues); }; @@ -147,7 +206,7 @@ const AsyncSelectList: FC = ({ notFoundContent={isLoading ? : null} optionLabelProp="label" style={{ width: '100%' }} - tagRender={tagRender} + tagRender={customTagRender} onBlur={() => { setCurrentPage(1); setSearchValue(''); @@ -160,6 +219,7 @@ const AsyncSelectList: FC = ({ {...props}> {tagOptions.map(({ label, value, displayName, data }) => ( { return (
@@ -60,6 +62,7 @@ export const EntityHeader = ({ { const { t } = useTranslation(); const location = useLocation(); @@ -63,8 +65,9 @@ const EntityHeaderTitle = ({ - {stringToHTML(displayName || name)} + ellipsis={{ tooltip: true }} + style={{ color: color ?? TEXT_COLOR }}> + {stringToHTML(displayName ?? name)} {openEntityInNewPage && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx index 5ad131ffb57..2f7ffa520a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsContainerV2/TagsContainerV2.tsx @@ -40,6 +40,7 @@ import { getRequestTagsPath, getUpdateTagsPath, } from '../../../utils/TasksUtils'; +import { SelectOption } from '../../AsyncSelectList/AsyncSelectList.interface'; import TagSelectForm from '../TagsSelectForm/TagsSelectForm.component'; import TagsV1 from '../TagsV1/TagsV1.component'; import TagsViewer from '../TagsViewer/TagsViewer'; @@ -74,11 +75,17 @@ const TagsContainerV2 = ({ showAddTagButton, selectedTagsInternal, isHoriZontalLayout, + initialOptions, } = useMemo( () => ({ isGlossaryType: tagType === TagSource.Glossary, showAddTagButton: permission && isEmpty(tags?.[tagType]), selectedTagsInternal: tags?.[tagType].map(({ tagFQN }) => tagFQN), + initialOptions: tags?.[tagType].map((data) => ({ + label: data.tagFQN, + value: data.tagFQN, + data, + })) as SelectOption[], isHoriZontalLayout: layoutType === LayoutType.HORIZONTAL, }), [tagType, permission, tags?.[tagType], tags, layoutType] @@ -207,6 +214,7 @@ const TagsContainerV2 = ({ defaultValue={selectedTagsInternal ?? []} fetchApi={fetchAPI} placeholder={getTagPlaceholder(isGlossaryType)} + tagData={initialOptions} onCancel={handleCancel} onSubmit={handleSave} /> @@ -218,6 +226,7 @@ const TagsContainerV2 = ({ fetchAPI, handleCancel, handleSave, + initialOptions, ]); const handleTagsTask = (hasTags: boolean) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx index ac5688e2882..b808554185f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx @@ -15,6 +15,7 @@ import { Button, Col, Form, Row, Space } from 'antd'; import { useForm } from 'antd/lib/form/Form'; import React, { useState } from 'react'; import AsyncSelectList from '../../../components/AsyncSelectList/AsyncSelectList'; +import './tag-select-fom.style.less'; import { TagsSelectFormProps } from './TagsSelectForm.interface'; const TagSelectForm = ({ @@ -23,6 +24,7 @@ const TagSelectForm = ({ placeholder, onSubmit, onCancel, + tagData, }: TagsSelectFormProps) => { const [form] = useForm(); const [isSubmitLoading, setIsSubmitLoading] = useState(false); @@ -62,7 +64,9 @@ const TagSelectForm = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.interface.ts index 44c7fe9d84d..15e932d24bc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.interface.ts @@ -18,6 +18,7 @@ import { Paging } from '../../../generated/type/paging'; export type TagsSelectFormProps = { placeholder: string; defaultValue: string[]; + tagData?: SelectOption[]; onChange?: (value: string[]) => void; onSubmit: (option: DefaultOptionType | DefaultOptionType[]) => Promise; onCancel: () => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/tag-select-fom.style.less b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/tag-select-fom.style.less new file mode 100644 index 00000000000..e317289cfc3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/tag-select-fom.style.less @@ -0,0 +1,19 @@ +/* + * 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 (reference) '/src/styles/variables.less'; + +.tag-select-box.ant-select-item-option-selected { + background-color: @grey-1; + display: flex; + align-items: center; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.component.tsx index 2994b37562a..ff3df3f2ee7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.component.tsx @@ -36,6 +36,7 @@ const TagsV1 = ({ className, showOnlyName = false, isVersionPage = false, + tagProps, }: TagsV1Props) => { const history = useHistory(); const color = useMemo( @@ -121,7 +122,8 @@ const TagsV1 = ({ + data-testid={`tag-${tag.tagFQN}`} + style={{ color: tag.style?.color }}> {tagName}
@@ -137,10 +139,11 @@ const TagsV1 = ({ data-testid="tags" style={ color - ? { backgroundColor: reduceColorOpacity(color, 0.1) } + ? { backgroundColor: reduceColorOpacity(color, 0.05) } : undefined } - onClick={() => redirectLink()}> + onClick={redirectLink} + {...tagProps}> {tagContent} ), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.interface.ts index 9895d153712..407ef452087 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.interface.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { TagProps } from 'antd'; import { TAG_START_WITH } from '../../../constants/Tag.constants'; import { TagLabel } from '../../../generated/type/tagLabel'; @@ -20,4 +21,5 @@ export type TagsV1Props = { showOnlyName?: boolean; className?: string; isVersionPage?: boolean; + tagProps?: TagProps; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Color.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Color.constants.ts index 3a14fb2ceb7..b365cc944ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Color.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Color.constants.ts @@ -17,3 +17,4 @@ export const GREEN_3_OPACITY = '#48ca9e30'; export const YELLOW_2 = '#ffbe0e'; export const RED_3 = '#f24822'; export const PURPLE_2 = '#7147e8'; +export const TEXT_COLOR = '#292929'; diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/app.less b/openmetadata-ui/src/main/resources/ui/src/styles/app.less index 518259adb22..8e40a89696c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/app.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/app.less @@ -373,7 +373,7 @@ a[href].link-text-grey, font-weight: 700; font-size: 18px; line-height: 22px; - color: @text-color !important; + color: @text-color; text-decoration: none !important; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ClassificationUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ClassificationUtils.tsx index 89dcbfbb9d2..b1361452bb0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ClassificationUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ClassificationUtils.tsx @@ -56,8 +56,15 @@ export const getCommonColumns = (): ColumnsType => [ key: 'name', width: 200, render: (_, record) => ( - - {record.name} + + {record.style?.iconURL && ( + + )} + + {record.name} + {record.disabled ? ( { }); it('should reduce color opacity by the given value', () => { - expect(reduceColorOpacity('#0000FF', 0)).toBe('#0000FFFF'); - expect(reduceColorOpacity('#00FF00', 0.25)).toBe('#00FF0040'); - expect(reduceColorOpacity('#FF0000', 0.5)).toBe('#FF000080'); - expect(reduceColorOpacity('#FF0000', 0.75)).toBe('#FF0000BF'); - expect(reduceColorOpacity('#FF0000', -0.5)).toBe('#FF00000'); + expect(reduceColorOpacity('#0000FF', 0)).toBe('rgba(0, 0, 255, 0)'); + expect(reduceColorOpacity('#00FF00', 0.25)).toBe('rgba(0, 255, 0, 0.25)'); + expect(reduceColorOpacity('#FF0000', 0.5)).toBe('rgba(255, 0, 0, 0.5)'); + expect(reduceColorOpacity('#FF0000', 0.75)).toBe('rgba(255, 0, 0, 0.75)'); + expect(reduceColorOpacity('#FF0000', -0.5)).toBe('rgba(255, 0, 0, -0.5)'); + expect(reduceColorOpacity('#FF0000', 0.05)).toBe('rgba(255, 0, 0, 0.05)'); }); it('should return base64 encoded string for input text', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index 66af2512f02..d0723b7ed4b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -782,14 +782,18 @@ export const getIsErrorMatch = (error: AxiosError, key: string): boolean => { }; /** - * @param color have color code + * @param color hex have color code * @param opacity take opacity how much to reduce it * @returns hex color string */ -export const reduceColorOpacity = (color: string, opacity: number): string => { - const _opacity = Math.round(Math.min(Math.max(opacity || 1, 0), 1) * 255); +export const reduceColorOpacity = (hex: string, opacity: number): string => { + hex = hex.replace(/^#/, ''); // Remove the "#" if it's there + hex = hex.length === 3 ? hex.replace(/./g, '$&$&') : hex; // Expand short hex to full hex format + const [red, green, blue] = [0, 2, 4].map((i) => + parseInt(hex.slice(i, i + 2), 16) + ); // Parse hex values - return color + _opacity.toString(16).toUpperCase(); + return `rgba(${red}, ${green}, ${blue}, ${opacity})`; // Create RGBA color }; export const getEntityDetailLink = (