mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-30 01:59:23 +00:00
ui: glossary badge feature feedback (#13562)
This commit is contained in:
parent
1c5a6e9425
commit
6111e62466
@ -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,
|
||||
|
||||
@ -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<AsyncSelectListProps> = ({
|
||||
onChange,
|
||||
fetchOptions,
|
||||
debounceTimeout = 800,
|
||||
initialData,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -39,6 +52,7 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [paging, setPaging] = useState<Paging>({} as Paging);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const selectedTagsRef = useRef<SelectOption[]>(initialData ?? []);
|
||||
|
||||
const loadOptions = useCallback(
|
||||
async (value: string) => {
|
||||
@ -84,7 +98,11 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
||||
className="text-grey-muted m-0 p-0">
|
||||
{parts.join(FQN_SEPARATOR_CHAR)}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Text ellipsis>{lastPartOfTag}</Typography.Text>
|
||||
<Typography.Text
|
||||
ellipsis
|
||||
style={{ color: tag.data?.style?.color }}>
|
||||
{lastPartOfTag}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
value: tag.value,
|
||||
@ -124,15 +142,56 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
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<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const tagProps = {
|
||||
closable: true,
|
||||
closeIcon: (
|
||||
<CloseOutlined
|
||||
className="p-r-xs"
|
||||
data-testid="remove-tags"
|
||||
height={8}
|
||||
width={8}
|
||||
/>
|
||||
),
|
||||
'data-testid': `selected-tag-${tagLabel}`,
|
||||
onClose,
|
||||
onMouseDown: onPreventMouseDown,
|
||||
} as TagProps;
|
||||
|
||||
return (
|
||||
<TagsV1
|
||||
startWith={TAG_START_WITH.SOURCE_ICON}
|
||||
tag={selectedTag?.data as TagLabel}
|
||||
tagProps={tagProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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<AsyncSelectListProps> = ({
|
||||
notFoundContent={isLoading ? <Loader size="small" /> : null}
|
||||
optionLabelProp="label"
|
||||
style={{ width: '100%' }}
|
||||
tagRender={tagRender}
|
||||
tagRender={customTagRender}
|
||||
onBlur={() => {
|
||||
setCurrentPage(1);
|
||||
setSearchValue('');
|
||||
@ -160,6 +219,7 @@ const AsyncSelectList: FC<AsyncSelectListProps> = ({
|
||||
{...props}>
|
||||
{tagOptions.map(({ label, value, displayName, data }) => (
|
||||
<Select.Option
|
||||
className={className}
|
||||
data={data}
|
||||
data-testid={`tag-${value}`}
|
||||
key={label}
|
||||
|
||||
@ -33,6 +33,7 @@ interface Props {
|
||||
openEntityInNewPage?: boolean;
|
||||
gutter?: 'default' | 'large';
|
||||
serviceName: string;
|
||||
titleColor?: string;
|
||||
badge?: React.ReactNode;
|
||||
}
|
||||
|
||||
@ -46,6 +47,7 @@ export const EntityHeader = ({
|
||||
gutter = 'default',
|
||||
serviceName,
|
||||
badge,
|
||||
titleColor,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
@ -60,6 +62,7 @@ export const EntityHeader = ({
|
||||
|
||||
<EntityHeaderTitle
|
||||
badge={badge}
|
||||
color={titleColor}
|
||||
deleted={entityData.deleted}
|
||||
displayName={entityData.displayName}
|
||||
icon={icon}
|
||||
|
||||
@ -17,6 +17,7 @@ import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { ReactComponent as IconExternalLink } from '../../../assets/svg/external-link-grey.svg';
|
||||
import { TEXT_COLOR } from '../../../constants/Color.constants';
|
||||
import { ROUTES } from '../../../constants/constants';
|
||||
import { stringToHTML } from '../../../utils/StringsUtils';
|
||||
import { EntityHeaderTitleProps } from './EntityHeaderTitle.interface';
|
||||
@ -32,6 +33,7 @@ const EntityHeaderTitle = ({
|
||||
badge,
|
||||
isDisabled,
|
||||
className,
|
||||
color,
|
||||
}: EntityHeaderTitleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
@ -63,8 +65,9 @@ const EntityHeaderTitle = ({
|
||||
<Typography.Text
|
||||
className="m-b-0 d-block entity-header-display-name text-lg font-semibold"
|
||||
data-testid="entity-header-display-name"
|
||||
ellipsis={{ tooltip: true }}>
|
||||
{stringToHTML(displayName || name)}
|
||||
ellipsis={{ tooltip: true }}
|
||||
style={{ color: color ?? TEXT_COLOR }}>
|
||||
{stringToHTML(displayName ?? name)}
|
||||
{openEntityInNewPage && (
|
||||
<IconExternalLink
|
||||
className="anticon vertical-baseline m-l-xss"
|
||||
|
||||
@ -16,6 +16,7 @@ export interface EntityHeaderTitleProps {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
link?: string;
|
||||
color?: string;
|
||||
openEntityInNewPage?: boolean;
|
||||
deleted?: boolean;
|
||||
serviceName: string;
|
||||
|
||||
@ -492,6 +492,11 @@ const GlossaryHeader = ({
|
||||
entityType={EntityType.GLOSSARY_TERM}
|
||||
icon={icon}
|
||||
serviceName=""
|
||||
titleColor={
|
||||
isGlossary
|
||||
? undefined
|
||||
: (selectedData as GlossaryTerm).style?.color
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="360px">
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 = ({
|
||||
<Col className="gutter-row" span={24}>
|
||||
<Form.Item noStyle name="tags">
|
||||
<AsyncSelectList
|
||||
className="tag-select-box"
|
||||
fetchOptions={fetchApi}
|
||||
initialData={tagData}
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
||||
@ -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<void>;
|
||||
onCancel: () => void;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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 = ({
|
||||
<Typography.Paragraph
|
||||
ellipsis
|
||||
className="m-0 tags-label"
|
||||
data-testid={`tag-${tag.tagFQN}`}>
|
||||
data-testid={`tag-${tag.tagFQN}`}
|
||||
style={{ color: tag.style?.color }}>
|
||||
{tagName}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
@ -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}
|
||||
</Tag>
|
||||
),
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -56,8 +56,15 @@ export const getCommonColumns = (): ColumnsType<Tag> => [
|
||||
key: 'name',
|
||||
width: 200,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Typography.Text>{record.name}</Typography.Text>
|
||||
<Space align="center">
|
||||
{record.style?.iconURL && (
|
||||
<img data-testid="tag-icon" src={record.style.iconURL} width={16} />
|
||||
)}
|
||||
<Typography.Text
|
||||
className="m-b-0"
|
||||
style={{ color: record.style?.color }}>
|
||||
{record.name}
|
||||
</Typography.Text>
|
||||
{record.disabled ? (
|
||||
<Badge
|
||||
className="m-l-xs badge-grey"
|
||||
|
||||
@ -243,11 +243,12 @@ describe('Tests for CommonUtils', () => {
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
||||
@ -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 = (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user