ui: glossary badge feature feedback (#13562)

This commit is contained in:
Shailesh Parmar 2023-10-13 17:10:40 +05:30 committed by GitHub
parent 1c5a6e9425
commit 6111e62466
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 151 additions and 26 deletions

View File

@ -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,

View File

@ -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}

View File

@ -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}

View File

@ -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"

View File

@ -16,6 +16,7 @@ export interface EntityHeaderTitleProps {
name: string;
displayName?: string;
link?: string;
color?: string;
openEntityInNewPage?: boolean;
deleted?: boolean;
serviceName: string;

View File

@ -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">

View File

@ -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) => {

View File

@ -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}
/>

View File

@ -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;

View File

@ -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;
}

View File

@ -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>
),

View File

@ -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;
};

View File

@ -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';

View File

@ -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;
}

View File

@ -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"

View File

@ -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', () => {

View File

@ -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 = (