chore(ui): UI improvements (#10277)

* chore(ui): UI improvements

* fix unit test issue

* changes as per comments
This commit is contained in:
Ashish Gupta 2023-02-23 13:05:10 +05:30 committed by GitHub
parent 61c896964e
commit ef30577ace
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 148 additions and 107 deletions

View File

@ -0,0 +1,11 @@
<svg
aria-hidden="true"
fill="currentColor"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg">
<path
d="M8.00003 11C7.87216 11 7.74416 10.9512 7.64653 10.8535L2.64653 5.85353C2.45116 5.65816 2.45116 5.34178 2.64653 5.14653C2.84191 4.95128 3.15828 4.95116 3.35353 5.14653L8.00003 9.79303L12.6465 5.14653C12.8419 4.95116 13.1583 4.95116 13.3535 5.14653C13.5488 5.34191 13.5489 5.65828 13.3535 5.85353L8.35354 10.8535C8.25591 10.9512 8.12791 11 8.00003 11Z"
strokeWidth="0.4"
/>
</svg>

After

Width:  |  Height:  |  Size: 546 B

View File

@ -171,7 +171,7 @@ const BotListV1 = ({
* handle after delete bot action
*/
const handleDeleteAction = useCallback(async () => {
fetchBots();
fetchBots(showDeleted);
}, [selectedUser]);
const handleSearch = (text: string) => {
@ -306,6 +306,7 @@ const BotListV1 = ({
<DeleteWidgetModal
afterDeleteAction={handleDeleteAction}
allowSoftDelete={!showDeleted}
entityId={selectedUser?.id || ''}
entityName={selectedUser?.displayName || ''}
entityType={EntityType.BOT}

View File

@ -11,7 +11,6 @@
* limitations under the License.
*/
import { CheckOutlined } from '@ant-design/icons';
import {
Button,
Form,
@ -24,6 +23,7 @@ import {
} from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { LOADING_STATE } from 'enums/common.enum';
import { isUndefined, trim } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -984,23 +984,14 @@ const CreateUser = ({
<Button data-testid="cancel-user" type="link" onClick={onCancel}>
{t('label.cancel')}
</Button>
<>
{saveState === 'waiting' ? (
<Button disabled type="primary">
<Loader size="small" type="white" />
</Button>
) : saveState === 'success' ? (
<Button disabled icon={<CheckOutlined />} type="primary" />
) : (
<Button
data-testid="save-user"
form="create-user-bot-form"
htmlType="submit"
type="primary">
{t('label.create')}
</Button>
)}
</>
<Button
data-testid="save-user"
form="create-user-bot-form"
htmlType="submit"
loading={saveState === LOADING_STATE.WAITING}
type="primary">
{t('label.create')}
</Button>
</Space>
</Form>
</div>

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { LOADING_STATE } from 'enums/common.enum';
import { CustomProperty, Type } from '../../generated/entity/type';
export interface CustomPropertyTableProp {
@ -19,6 +20,5 @@ export interface CustomPropertyTableProp {
updateEntityType: (
customProperties: Type['customProperties']
) => Promise<void>;
loadingState: LOADING_STATE;
}
export type Operation = 'delete' | 'update' | 'no-operation';

View File

@ -18,6 +18,7 @@ import {
render,
screen,
} from '@testing-library/react';
import { LOADING_STATE } from 'enums/common.enum';
import React from 'react';
import { CustomPropertyTable } from './CustomPropertyTable';
@ -64,6 +65,7 @@ const mockProp = {
hasAccess: true,
customProperties: mockProperties,
updateEntityType: mockUpdateEntityType,
loadingState: LOADING_STATE.INITIAL,
};
describe('Test CustomField Table Component', () => {

View File

@ -10,38 +10,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Table, Tooltip } from 'antd';
import { Button, Space, Table, Tooltip } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { LOADING_STATE, OPERATION } from 'enums/common.enum';
import { isEmpty } from 'lodash';
import React, { FC, Fragment, useMemo, useState } from 'react';
import React, { FC, Fragment, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as IconDelete } from '../../assets/svg/ic-delete.svg';
import { ReactComponent as IconEdit } from '../../assets/svg/ic-edit.svg';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { CustomProperty } from '../../generated/entity/type';
import { getEntityName } from '../../utils/CommonUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import {
CustomPropertyTableProp,
Operation,
} from './CustomPropertyTable.interface';
import { CustomPropertyTableProp } from './CustomPropertyTable.interface';
export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
customProperties,
updateEntityType,
hasAccess,
loadingState,
}) => {
const { t } = useTranslation();
const [selectedProperty, setSelectedProperty] = useState<CustomProperty>(
{} as CustomProperty
);
const [operation, setOperation] = useState<Operation>('no-operation');
const [operation, setOperation] = useState<OPERATION>(OPERATION.NO_OPERATION);
const resetSelectedProperty = () => {
setSelectedProperty({} as CustomProperty);
setOperation('no-operation' as Operation);
setOperation(OPERATION.NO_OPERATION);
};
const handlePropertyDelete = () => {
@ -49,9 +49,14 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
(property) => property.name !== selectedProperty.name
);
updateEntityType(updatedProperties);
resetSelectedProperty();
};
useEffect(() => {
if (loadingState === LOADING_STATE.INITIAL) {
resetSelectedProperty();
}
}, [loadingState]);
const handlePropertyUpdate = async (updatedDescription: string) => {
const updatedProperties = customProperties.map((property) => {
if (property.name === selectedProperty.name) {
@ -64,8 +69,14 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
resetSelectedProperty();
};
const deleteCheck = !isEmpty(selectedProperty) && operation === 'delete';
const updateCheck = !isEmpty(selectedProperty) && operation === 'update';
const deleteCheck = useMemo(
() => !isEmpty(selectedProperty) && operation === OPERATION.DELETE,
[selectedProperty, operation]
);
const updateCheck = useMemo(
() => !isEmpty(selectedProperty) && operation === OPERATION.UPDATE,
[selectedProperty, operation]
);
const tableColumn: ColumnsType<CustomProperty> = useMemo(
() => [
@ -100,44 +111,36 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
dataIndex: 'actions',
key: 'actions',
render: (_, record) => (
<div className="tw-flex">
<Tooltip
title={hasAccess ? t('label.edit') : NO_PERMISSION_FOR_ACTION}>
<button
className="tw-cursor-pointer"
<Space align="center" size={14}>
<Tooltip title={!hasAccess && NO_PERMISSION_FOR_ACTION}>
<Button
className="cursor-pointer p-0"
data-testid="edit-button"
disabled={!hasAccess}
size="small"
type="text"
onClick={() => {
setSelectedProperty(record);
setOperation('update');
setOperation(OPERATION.UPDATE);
}}>
<SVGIcons
alt="edit"
icon={Icons.EDIT}
title={t('label.edit')}
width="16px"
/>
</button>
<IconEdit name={t('label.edit')} width={16} />
</Button>
</Tooltip>
<Tooltip
title={hasAccess ? t('label.delete') : NO_PERMISSION_FOR_ACTION}>
<button
className="tw-cursor-pointer tw-ml-4"
<Tooltip title={!hasAccess && NO_PERMISSION_FOR_ACTION}>
<Button
className="cursor-pointer p-0"
data-testid="delete-button"
disabled={!hasAccess}
size="small"
type="text"
onClick={() => {
setSelectedProperty(record);
setOperation('delete');
setOperation(OPERATION.DELETE);
}}>
<SVGIcons
alt="delete"
icon={Icons.DELETE}
title={t('label.delete')}
width="16px"
/>
</button>
<IconDelete name={t('label.delete')} width={16} />
</Button>
</Tooltip>
</div>
</Space>
),
},
],
@ -164,6 +167,7 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
header={t('label.delete-property-name', {
propertyName: selectedProperty.name,
})}
loadingState={loadingState}
visible={deleteCheck}
onCancel={resetSelectedProperty}
onConfirm={handlePropertyDelete}

View File

@ -15,9 +15,10 @@ import {
SortAscendingOutlined,
SortDescendingOutlined,
} from '@ant-design/icons';
import { Card, Col, Row, Tabs } from 'antd';
import { Button, Card, Col, Row, Space, Tabs } from 'antd';
import FacetFilter from 'components/common/facetfilter/FacetFilter';
import SearchedData from 'components/searched-data/SearchedData';
import { SORT_ORDER } from 'enums/common.enum';
import unique from 'fork-ts-checker-webpack-plugin/lib/utils/array/unique';
import {
isEmpty,
@ -95,6 +96,18 @@ const Explore: React.FC<ExploreProps> = ({
setShowSummaryPanel(false);
};
const isAscSortOrder = useMemo(
() => sortOrder === SORT_ORDER.ASC,
[sortOrder]
);
const sortProps = useMemo(
() => ({
className: 'text-base text-primary',
'data-testid': 'last-updated',
}),
[]
);
const tabItems = useMemo(
() =>
Object.entries(tabsInfo).map(([tabSearchIndex, tabDetail]) => ({
@ -261,31 +274,28 @@ const Explore: React.FC<ExploreProps> = ({
items={tabItems}
size="small"
tabBarExtraContent={
<div className="tw-flex">
<Space align="center" size={4}>
<SortingDropDown
fieldList={tabsInfo[searchIndex].sortingFields}
handleFieldDropDown={onChangeSortValue}
sortField={sortValue}
/>
<div className="tw-flex">
{sortOrder === 'asc' ? (
<button onClick={() => onChangeSortOder('desc')}>
<SortAscendingOutlined
className="tw-text-base tw-text-primary"
data-testid="last-updated"
/>
</button>
<Button
className="p-0"
size="small"
type="text"
onClick={() =>
onChangeSortOder(
isAscSortOrder ? SORT_ORDER.DESC : SORT_ORDER.ASC
)
}>
{isAscSortOrder ? (
<SortAscendingOutlined {...sortProps} />
) : (
<button onClick={() => onChangeSortOder('asc')}>
<SortDescendingOutlined
className="tw-text-base tw-text-primary"
data-testid="last-updated"
/>
</button>
<SortDescendingOutlined {...sortProps} />
)}
</div>
</div>
</Button>
</Space>
}
onChange={(tab) => {
tab && onChangeSearchIndex(tab as ExploreSearchIndex);

View File

@ -11,10 +11,9 @@
* limitations under the License.
*/
import { Dropdown } from 'antd';
import { Dropdown, Space, Typography } from 'antd';
import React from 'react';
import { normalLink } from '../../utils/styleconstant';
import { dropdownIcon as DropDownIcon } from '../../utils/svgconstant';
import { ReactComponent as DropDownIcon } from '../../assets/svg/bottom-arrow.svg';
export interface SortingField {
name: string;
@ -43,16 +42,16 @@ const SortingDropDown: React.FC<SortingDropdownProps> = ({
return (
<Dropdown
className="tw-self-end tw-mr-2 tw-cursor-pointer"
className="self-end m-r-xs cursor-pointer"
data-testid="dropdown"
menu={{
items,
}}
trigger={['click']}>
<div className="tw-text-primary" data-testid="dropdown-label">
<span className="tw-mr-2">{label}</span>
<DropDownIcon style={{ color: normalLink, margin: '0px' }} />
</div>
<Space align="center" data-testid="dropdown-label" size={4}>
<Typography.Text className="text-primary">{label}</Typography.Text>
<DropDownIcon className="text-primary" height={16} width={16} />
</Space>
</Dropdown>
);
};

View File

@ -12,6 +12,7 @@
*/
import { DefaultOptionType } from 'antd/lib/select';
import { SORT_ORDER } from 'enums/common.enum';
import { JsonTree } from 'react-awesome-query-builder';
import { SearchIndex } from '../../enums/search.enum';
import { Dashboard } from '../../generated/entity/data/dashboard';
@ -65,7 +66,7 @@ export interface ExploreProps {
onChangeSortValue: (sortValue: string) => void;
sortOrder: string;
onChangeSortOder: (sortOder: string) => void;
onChangeSortOder: (sortOder: SORT_ORDER) => void;
showDeleted: boolean;
onChangeShowDeleted: (showDeleted: boolean) => void;

View File

@ -14,12 +14,13 @@
import { Button, Typography } from 'antd';
import Modal from 'antd/lib/modal/Modal';
import classNames from 'classnames';
import { LOADING_STATE } from 'enums/common.enum';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ConfirmationModalProps } from './ConfirmationModal.interface';
const ConfirmationModal = ({
loadingState = 'initial',
loadingState = LOADING_STATE.INITIAL,
cancelText,
confirmText,
header,
@ -57,9 +58,12 @@ const ConfirmationModal = ({
className={confirmButtonCss}
danger={confirmText === t('label.delete')}
data-testid={
loadingState === 'waiting' ? 'loading-button' : 'save-button'
loadingState === LOADING_STATE.WAITING
? 'loading-button'
: 'save-button'
}
key="save-btn"
loading={LOADING_STATE.WAITING === loadingState}
type="primary"
onClick={onConfirm}>
{confirmText}

View File

@ -37,3 +37,14 @@ export enum PROMISE_STATE {
FULFILLED = 'fulfilled',
REJECTED = 'rejected',
}
export enum OPERATION {
UPDATE = 'update',
DELETE = 'delete',
NO_OPERATION = 'no-operation',
}
export enum SORT_ORDER {
ASC = 'asc',
DESC = 'desc',
}

View File

@ -14,6 +14,7 @@
import { AxiosError } from 'axios';
import PageContainerV1 from 'components/containers/PageContainerV1';
import CreateUserComponent from 'components/CreateUser/CreateUser.component';
import { LOADING_STATE } from 'enums/common.enum';
import _ from 'lodash';
import { observer } from 'mobx-react';
import { LoadingState } from 'Models';
@ -38,7 +39,7 @@ const CreateUserPage = () => {
const { t } = useTranslation();
const [roles, setRoles] = useState<Array<Role>>([]);
const [status, setStatus] = useState<LoadingState>('initial');
const [status, setStatus] = useState<LoadingState>(LOADING_STATE.INITIAL);
const { bot } = useParams<{ bot: string }>();
@ -69,7 +70,7 @@ const CreateUserPage = () => {
fallbackText?: string
) => {
showErrorToast(error, fallbackText);
setStatus('initial');
setStatus(LOADING_STATE.INITIAL);
};
const checkBotInUse = async (name: string) => {
@ -87,15 +88,16 @@ const CreateUserPage = () => {
* @param userData Data for creating new user
*/
const handleAddUserSave = async (userData: CreateUser) => {
setStatus(LOADING_STATE.WAITING);
if (bot) {
const isBotExists = await checkBotInUse(userData.name);
if (isBotExists) {
setStatus(LOADING_STATE.INITIAL);
showErrorToast(
t('message.entity-already-exists', { entity: userData.name })
);
} else {
try {
setStatus('waiting');
// Create a user with isBot:true
const userResponse = await createUserWithPut({
...userData,
@ -111,12 +113,12 @@ const CreateUserPage = () => {
});
if (botResponse) {
setStatus('success');
setStatus(LOADING_STATE.SUCCESS);
showSuccessToast(
t('server.create-entity-success', { entity: t('label.bot') })
);
setTimeout(() => {
setStatus('initial');
setStatus(LOADING_STATE.INITIAL);
goToUserListPage();
}, 500);
@ -134,14 +136,12 @@ const CreateUserPage = () => {
}
} else {
try {
setStatus('waiting');
const response = await createUser(userData);
if (response) {
setStatus('success');
setStatus(LOADING_STATE.SUCCESS);
setTimeout(() => {
setStatus('initial');
setStatus(LOADING_STATE.WAITING);
goToUserListPage();
}, 500);
} else {

View File

@ -24,6 +24,7 @@ import {
ResourceEntity,
} from 'components/PermissionProvider/PermissionProvider.interface';
import SchemaEditor from 'components/schema-editor/SchemaEditor';
import { LOADING_STATE } from 'enums/common.enum';
import { compare } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash';
import { default as React, useEffect, useMemo, useState } from 'react';
@ -57,6 +58,8 @@ const CustomEntityDetailV1 = () => {
const [selectedEntityTypeDetail, setSelectedEntityTypeDetail] =
useState<Type>({} as Type);
const [loadingState, setLoadingState] = useState(LOADING_STATE.INITIAL);
const tabAttributePath = ENTITY_PATH[tab.toLowerCase()];
const { getEntityPermission } = usePermissionProvider();
@ -126,6 +129,7 @@ const CustomEntityDetailV1 = () => {
}, [selectedEntityTypeDetail]);
const updateEntityType = async (properties: Type['customProperties']) => {
setLoadingState(LOADING_STATE.WAITING);
const patch = compare(selectedEntityTypeDetail, {
...selectedEntityTypeDetail,
customProperties: properties,
@ -139,6 +143,8 @@ const CustomEntityDetailV1 = () => {
}));
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setLoadingState(LOADING_STATE.INITIAL);
}
};
@ -275,6 +281,7 @@ const CustomEntityDetailV1 = () => {
selectedEntityTypeDetail.customProperties || []
}
hasAccess={editPermission}
loadingState={loadingState}
updateEntityType={updateEntityType}
/>
</div>

View File

@ -19,6 +19,7 @@ import {
SearchHitCounts,
UrlParams,
} from 'components/Explore/explore.interface';
import { SORT_ORDER } from 'enums/common.enum';
import { isNil, isString } from 'lodash';
import Qs from 'qs';
import React, { FunctionComponent, useEffect, useMemo, useState } from 'react';
@ -30,7 +31,6 @@ import AppState from '../../AppState';
import { getExplorePath, PAGE_SIZE } from '../../constants/constants';
import {
INITIAL_SORT_FIELD,
INITIAL_SORT_ORDER,
tabsInfo,
} from '../../constants/explore.constants';
import { SearchIndex } from '../../enums/search.enum';
@ -57,7 +57,7 @@ const ExplorePage: FunctionComponent = () => {
const [sortValue, setSortValue] = useState<string>(INITIAL_SORT_FIELD);
const [sortOrder, setSortOrder] = useState<string>(INITIAL_SORT_ORDER);
const [sortOrder, setSortOrder] = useState<SORT_ORDER>(SORT_ORDER.DESC);
const [searchHitCounts, setSearchHitCounts] = useState<SearchHitCounts>();

View File

@ -628,11 +628,8 @@ const TagsPage = () => {
<div className="tw-mb-3">
<Tooltip
title={
createClassificationPermission
? t('label.add-entity', {
entity: t('label.classification'),
})
: t('message.no-permission-for-action')
!createClassificationPermission &&
t('message.no-permission-for-action')
}>
<Button
block
@ -665,7 +662,7 @@ const TagsPage = () => {
key={category.name}
onClick={() => onClickClassifications(category)}>
<Typography.Paragraph
className="ant-typography-ellipsis-custom tag-category label-category self-center w-32"
className="ant-typography-ellipsis-custom tag-category label-category self-center"
data-testid="tag-name"
ellipsis={{ rows: 1, tooltip: true }}>
{getEntityName(category as unknown as EntityReference)}

View File

@ -89,10 +89,13 @@
gap: 16px;
}
// Alignment Items
.align-middle {
vertical-align: middle;
}
.self-end {
align-self: flex-end;
}
.vertical-align-inherit {
vertical-align: inherit;
}