feat: improve dashboard view user experience (#3654)

* feat: improve dashboard view user experience

* chore: dashboard ux is updated

* feat: add inter font and set font family in theme configuration

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
Yunus M 2023-11-20 14:53:13 +05:30 committed by GitHub
parent 5d6eea3045
commit 2a55f3d680
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 58 deletions

View File

@ -1,5 +1,5 @@
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { Modal, Tooltip } from 'antd'; import { Modal, Tooltip, Typography } from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard'; import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
import { useCallback } from 'react'; import { useCallback } from 'react';
@ -10,10 +10,22 @@ import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles'; import { USER_ROLES } from 'types/roles';
import { Data } from '../index'; import { Data } from '..';
import { TableLinkText } from './styles'; import { TableLinkText } from './styles';
function DeleteButton({ id, createdBy, isLocked }: Data): JSX.Element { interface DeleteButtonProps {
createdBy: string;
name: string;
id: string;
isLocked: boolean;
}
function DeleteButton({
createdBy,
name,
id,
isLocked,
}: DeleteButtonProps): JSX.Element {
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { role, user } = useSelector<AppState, AppReducer>((state) => state.app); const { role, user } = useSelector<AppState, AppReducer>((state) => state.app);
const isAuthor = user?.email === createdBy; const isAuthor = user?.email === createdBy;
@ -26,7 +38,13 @@ function DeleteButton({ id, createdBy, isLocked }: Data): JSX.Element {
const openConfirmationDialog = useCallback((): void => { const openConfirmationDialog = useCallback((): void => {
modal.confirm({ modal.confirm({
title: 'Do you really want to delete this dashboard?', title: (
<Typography.Title level={5}>
Are you sure you want to delete the
<span style={{ color: '#e42b35', fontWeight: 500 }}> {name} </span>
dashboard?
</Typography.Title>
),
icon: <ExclamationCircleOutlined style={{ color: '#e42b35' }} />, icon: <ExclamationCircleOutlined style={{ color: '#e42b35' }} />,
onOk() { onOk() {
deleteDashboardMutation.mutateAsync(undefined, { deleteDashboardMutation.mutateAsync(undefined, {
@ -39,7 +57,7 @@ function DeleteButton({ id, createdBy, isLocked }: Data): JSX.Element {
okButtonProps: { danger: true }, okButtonProps: { danger: true },
centered: true, centered: true,
}); });
}, [modal, deleteDashboardMutation, queryClient]); }, [modal, name, deleteDashboardMutation, queryClient]);
const getDeleteTooltipContent = (): string => { const getDeleteTooltipContent = (): string => {
if (isLocked) { if (isLocked) {

View File

@ -1,11 +1,12 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { import {
Card, Card,
Col,
Dropdown, Dropdown,
Input,
MenuProps, MenuProps,
Row, Row,
TableColumnProps, TableColumnProps,
Typography,
} from 'antd'; } from 'antd';
import { ItemType } from 'antd/es/menu/hooks/useItems'; import { ItemType } from 'antd/es/menu/hooks/useItems';
import createDashboard from 'api/dashboard/create'; import createDashboard from 'api/dashboard/create';
@ -18,9 +19,9 @@ import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
import LabelColumn from 'components/TableRenderer/LabelColumn'; import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import SearchFilter from 'container/ListOfDashboard/SearchFilter';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import history from 'lib/history'; import history from 'lib/history';
import { Key, useCallback, useEffect, useMemo, useState } from 'react'; import { Key, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -38,6 +39,8 @@ import DeleteButton from './TableComponents/DeleteButton';
import Name from './TableComponents/Name'; import Name from './TableComponents/Name';
function ListOfAllDashboard(): JSX.Element { function ListOfAllDashboard(): JSX.Element {
const { Search } = Input;
const { const {
data: dashboardListResponse = [], data: dashboardListResponse = [],
isLoading: isDashboardListLoading, isLoading: isDashboardListLoading,
@ -59,12 +62,21 @@ function ListOfAllDashboard(): JSX.Element {
] = useState<boolean>(false); ] = useState<boolean>(false);
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false); const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
const [filteredDashboards, setFilteredDashboards] = useState<Dashboard[]>(); const [dashboards, setDashboards] = useState<Dashboard[]>();
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
const sortedDashboards = dashboards.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
);
setDashboards(sortedDashboards);
};
useEffect(() => { useEffect(() => {
if (dashboardListResponse.length) { if (dashboardListResponse.length) {
setFilteredDashboards(dashboardListResponse); sortDashboardsByCreatedAt(dashboardListResponse);
} }
}, [dashboardListResponse]); }, [dashboardListResponse]);
@ -150,7 +162,7 @@ function ListOfAllDashboard(): JSX.Element {
}, [action]); }, [action]);
const data: Data[] = const data: Data[] =
filteredDashboards?.map((e) => ({ dashboards?.map((e) => ({
createdAt: e.created_at, createdAt: e.created_at,
description: e.data.description || '', description: e.data.description || '',
id: e.uuid, id: e.uuid,
@ -255,11 +267,50 @@ function ListOfAllDashboard(): JSX.Element {
[getMenuItems], [getMenuItems],
); );
const searchArrayOfObjects = (searchValue: string): any[] => {
// Convert the searchValue to lowercase for case-insensitive search
const searchValueLowerCase = searchValue.toLowerCase();
// Use the filter method to find matching objects
return dashboardListResponse.filter((item: any) => {
// Convert each property value to lowercase for case-insensitive search
const itemValues = Object.values(item?.data).map((value: any) =>
value.toString().toLowerCase(),
);
// Check if any property value contains the searchValue
return itemValues.some((value) => value.includes(searchValueLowerCase));
});
};
const handleSearch = useDebouncedFn((event: unknown): void => {
setIsFilteringDashboards(true);
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = searchArrayOfObjects(searchText);
setDashboards(filteredDashboards);
setIsFilteringDashboards(false);
}, 500);
const GetHeader = useMemo( const GetHeader = useMemo(
() => ( () => (
<Row justify="space-between"> <Row gutter={16} align="middle">
<Typography>Dashboard List</Typography> <Col span={18}>
<Search
disabled={isDashboardListLoading}
placeholder="Search by Name, Description, Tags"
onChange={handleSearch}
loading={isFilteringDashboards}
style={{ marginBottom: 16, marginTop: 16 }}
/>
</Col>
<Col
span={6}
style={{
display: 'flex',
justifyContent: 'flex-end',
}}
>
<ButtonContainer> <ButtonContainer>
<TextToolTip <TextToolTip
{...{ {...{
@ -285,11 +336,15 @@ function ListOfAllDashboard(): JSX.Element {
</Dropdown> </Dropdown>
)} )}
</ButtonContainer> </ButtonContainer>
</Col>
</Row> </Row>
), ),
[ [
newDashboard, Search,
isDashboardListLoading, isDashboardListLoading,
handleSearch,
isFilteringDashboards,
newDashboard,
menu, menu,
newDashboardState.loading, newDashboardState.loading,
newDashboardState.error, newDashboardState.error,
@ -301,13 +356,6 @@ function ListOfAllDashboard(): JSX.Element {
<Card> <Card>
{GetHeader} {GetHeader}
{!isDashboardListLoading && (
<SearchFilter
searchData={dashboardListResponse}
filterDashboards={setFilteredDashboards}
/>
)}
<TableContainer> <TableContainer>
<ImportJSON <ImportJSON
isImportJSONModalVisible={isImportJSONModalVisible} isImportJSONModalVisible={isImportJSONModalVisible}
@ -319,8 +367,9 @@ function ListOfAllDashboard(): JSX.Element {
dynamicColumns={dynamicColumns} dynamicColumns={dynamicColumns}
columns={columns} columns={columns}
pagination={{ pagination={{
pageSize: 9, pageSize: 10,
defaultPageSize: 9, defaultPageSize: 10,
total: data?.length || 0,
}} }}
showHeader showHeader
bordered bordered

View File

@ -3,12 +3,16 @@ import { Tabs } from 'antd';
import GeneralDashboardSettings from './General'; import GeneralDashboardSettings from './General';
import VariablesSetting from './Variables'; import VariablesSetting from './Variables';
function DashboardSettingsContent(): JSX.Element {
const items = [ const items = [
{ label: 'General', key: 'general', children: <GeneralDashboardSettings /> }, {
label: 'General',
key: 'general',
children: <GeneralDashboardSettings />,
},
{ label: 'Variables', key: 'variables', children: <VariablesSetting /> }, { label: 'Variables', key: 'variables', children: <VariablesSetting /> },
]; ];
function DashboardSettingsContent(): JSX.Element {
return <Tabs items={items} />; return <Tabs items={items} />;
} }

View File

@ -68,6 +68,12 @@ export const getOptions = (routes: string): Option[] => {
return Options; return Options;
}; };
export const routesToHideBreadCrumbs = [
ROUTES.SUPPORT,
ROUTES.ALL_DASHBOARD,
ROUTES.DASHBOARD,
];
export const routesToSkip = [ export const routesToSkip = [
ROUTES.SETTINGS, ROUTES.SETTINGS,
ROUTES.LIST_ALL_ALERT, ROUTES.LIST_ALL_ALERT,

View File

@ -6,7 +6,11 @@ import { matchPath, useHistory } from 'react-router-dom';
import NewExplorerCTA from '../NewExplorerCTA'; import NewExplorerCTA from '../NewExplorerCTA';
import ShowBreadcrumbs from './Breadcrumbs'; import ShowBreadcrumbs from './Breadcrumbs';
import DateTimeSelector from './DateTimeSelection'; import DateTimeSelector from './DateTimeSelection';
import { routesToDisable, routesToSkip } from './DateTimeSelection/config'; import {
routesToDisable,
routesToHideBreadCrumbs,
routesToSkip,
} from './DateTimeSelection/config';
import { Container } from './styles'; import { Container } from './styles';
function TopNav(): JSX.Element | null { function TopNav(): JSX.Element | null {
@ -20,6 +24,14 @@ function TopNav(): JSX.Element | null {
[location.pathname], [location.pathname],
); );
const isRouteToHideBreadCrumbs = useMemo(
() =>
routesToHideBreadCrumbs.some((route) =>
matchPath(location.pathname, { path: route, exact: true }),
),
[location.pathname],
);
const isDisabled = useMemo( const isDisabled = useMemo(
() => () =>
routesToDisable.some((route) => routesToDisable.some((route) =>
@ -33,22 +45,20 @@ function TopNav(): JSX.Element | null {
[location.pathname], [location.pathname],
); );
const hideBreadcrumbs = location.pathname === ROUTES.SUPPORT;
if (isSignUpPage || isDisabled) { if (isSignUpPage || isDisabled) {
return null; return null;
} }
return ( return (
<Container> <Container>
{!hideBreadcrumbs && ( {!isRouteToHideBreadCrumbs && (
<Col span={16}> <Col span={16}>
<ShowBreadcrumbs /> <ShowBreadcrumbs />
</Col> </Col>
)} )}
{!isRouteToSkip && ( {!isRouteToSkip && (
<Col span={8}> <Col span={isRouteToHideBreadCrumbs ? 24 : 8}>
<Row justify="end"> <Row justify="end">
<Space align="start" size={60} direction="horizontal"> <Space align="start" size={60} direction="horizontal">
<NewExplorerCTA /> <NewExplorerCTA />

View File

@ -74,7 +74,8 @@ export const useThemeConfig = (): ThemeConfig => {
borderRadiusLG: 2, borderRadiusLG: 2,
borderRadiusSM: 2, borderRadiusSM: 2,
borderRadiusXS: 2, borderRadiusXS: 2,
fontFamily: 'Open Sans', fontFamily: 'Inter',
fontSize: 13,
}, },
}; };
}; };

View File

@ -62,10 +62,12 @@
href="https://unpkg.com/uplot@1.6.26/dist/uPlot.min.css" href="https://unpkg.com/uplot@1.6.26/dist/uPlot.min.css"
/> />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
</head> </head>