change(ui): dashboard redesign

This commit is contained in:
Shekar Siri 2024-06-26 10:13:06 +02:00
parent 3b2c988e42
commit 2ea07b045a
14 changed files with 262 additions and 119 deletions

View file

@ -1,6 +1,6 @@
import React from 'react';
import {Card, Col, Modal, Row, Typography} from "antd";
import {Grid2X2, Plus} from "lucide-react";
import {Grid2x2CheckIcon, Plus} from "lucide-react";
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
interface Props {
@ -32,8 +32,8 @@ function AddCardSelectionModal(props: Props) {
<Row gutter={16} justify="center">
<Col span={12}>
<Card hoverable onClick={() => onClick(true)}>
<div className="flex flex-col items-center justify-center">
<Grid2X2 style={{fontSize: '24px', color: '#1890ff'}}/>
<div className="flex flex-col items-center justify-center" style={{height: '80px'}}>
<Grid2x2CheckIcon style={{fontSize: '24px', color: '#1890ff'}}/>
<Typography.Text strong>Add from library</Typography.Text>
<p>Select from 12 available</p>
</div>
@ -41,7 +41,7 @@ function AddCardSelectionModal(props: Props) {
</Col>
<Col span={12}>
<Card hoverable onClick={() => onClick(false)}>
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center justify-center" style={{height: '80px'}}>
<Plus style={{fontSize: '24px', color: '#1890ff'}}/>
<p>Create New Card</p>
</div>

View file

@ -1,32 +1,63 @@
import React from 'react';
import DashboardSelectionModal from "Components/Dashboard/components/DashboardSelectionModal/DashboardSelectionModal";
import {Grid2x2Check} from "lucide-react"
import {Button} from "antd";
import {Button, Modal} from "antd";
import Select from "Shared/Select/Select";
import {Form} from "UI";
import {useStore} from "App/mstore";
interface Props {
metricId: string;
}
function AddToDashboardButton({metricId}: Props) {
const [show, setShow] = React.useState(false);
const {dashboardStore} = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id,
label: i.name,
value: i.dashboardId,
}));
const [selectedId, setSelectedId] = React.useState(dashboardOptions[0].value);
const onSave = (close: any) => {
const dashboard = dashboardStore.getDashboard(selectedId)
if (dashboard) {
dashboardStore.addWidgetToDashboard(dashboard, [metricId]).then(close)
}
}
const onClick = () => {
Modal.confirm({
title: 'Add to selected dashboard',
icon: null,
content: (
<Form.Field>
<Select
options={dashboardOptions}
defaultValue={dashboardOptions[0].value}
onChange={({value}: any) => setSelectedId(value.value)}
/>
</Form.Field>
),
cancelText: 'Cancel',
onOk: onSave,
okText: 'Add',
footer: (_, {OkBtn, CancelBtn}) => (
<>
<CancelBtn/>
<OkBtn/>
</>
),
})
}
return (
<>
<Button
type="default"
// className="ml-2 p-0"
onClick={() => setShow(true)}
icon={<Grid2x2Check size={18}/>}
>
Add to Dashboard
</Button>
{show && (
<DashboardSelectionModal
metricId={metricId}
show={show}
closeHandler={() => setShow(false)}
/>
)}
</>
<Button
type="default"
onClick={onClick}
icon={<Grid2x2Check size={18}/>}
>
Add to Dashboard
</Button>
);
}

View file

@ -17,10 +17,11 @@ interface Props {
category?: string;
selectedList: any;
onCard: (metricId: number) => void;
query?: string;
}
function CardsLibrary(props: Props) {
const {selectedList} = props;
const {selectedList, query = ''} = props;
const {metricStore, dashboardStore} = useStore();
// const cards = useMemo(() => {
@ -29,6 +30,12 @@ function CardsLibrary(props: Props) {
// });
// }, [metricStore.filteredCards, props.category]);
const cards = useMemo(() => {
return metricStore.filteredCards.filter((card: any) => {
return card.name.toLowerCase().includes(query.toLowerCase());
});
}, [query, metricStore.filteredCards]);
useEffect(() => {
metricStore.fetchList();
}, []);
@ -40,7 +47,7 @@ function CardsLibrary(props: Props) {
return (
<Loader loading={metricStore.isLoading}>
<div className="grid grid-cols-4 gap-4 items-start">
{metricStore.filteredCards.map((metric: any) => (
{cards.map((metric: any) => (
<React.Fragment key={metric.metricId}>
<div className={'col-span-' + metric.config.col}
onClick={() => onItemClick(metric.metricId)}>
@ -50,7 +57,12 @@ function CardsLibrary(props: Props) {
border: selectedList.includes(metric.metricId) ? '1px solid #1890ff' : '1px solid #f0f0f0',
}}
styles={{
header: {padding: '4px 14px', minHeight: '36px', fontSize: '14px'},
header: {
padding: '4px 14px',
minHeight: '36px',
fontSize: '14px',
borderBottom: 'none'
},
body: {padding: '14px'},
cover: {
border: '2px solid #1890ff',
@ -60,8 +72,8 @@ function CardsLibrary(props: Props) {
<WidgetChart
// isPreview={true}
metric={metric}
// isTemplate={true}
// isWidget={false}
isTemplate={true}
isWidget={true}
/>
</Card>
</LazyLoad>

View file

@ -7,21 +7,15 @@ interface NewDashboardModalProps {
onClose: () => void;
open: boolean;
isAddingFromLibrary?: boolean;
isCreatingNewCard?: boolean;
}
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
onClose,
open,
isAddingFromLibrary = false,
isCreatingNewCard = false
}) => {
const [step, setStep] = React.useState<number>(0);
const onCard = () => {
setStep(step + 1);
};
useEffect(() => {
return () => {
setStep(0);
@ -33,8 +27,10 @@ const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
<Modal open={open} onCancel={onClose} width={900} destroyOnClose={true} footer={null} closeIcon={false}>
<div>
<div className="flex flex-col gap-4">
{step === 0 && <SelectCard onClose={onClose} onCard={onCard} isLibrary={isAddingFromLibrary}/>}
{step === 1 && <CreateCard onBack={() => setStep(0)} />}
{step === 0 && <SelectCard onClose={onClose}
onCard={() => setStep(step + 1)}
isLibrary={isAddingFromLibrary}/>}
{step === 1 && <CreateCard onBack={() => setStep(0)}/>}
</div>
</div>
</Modal>

View file

@ -1,5 +1,5 @@
import React, {useMemo} from 'react';
import {Button, Segmented} from 'antd';
import {Button, Input, Segmented, Space} from 'antd';
import {CARD_LIST, CARD_CATEGORIES, CardType} from './ExampleCards';
import {useStore} from 'App/mstore';
import Option from './Option';
@ -17,6 +17,7 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
const [selectedCards, setSelectedCards] = React.useState<number[]>([]);
const {metricStore, dashboardStore} = useStore();
const dashboardId = window.location.pathname.split('/')[3];
const [libraryQuery, setLibraryQuery] = React.useState<string>('');
const handleCardSelection = (card: string) => {
@ -55,33 +56,59 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
return (
<>
<Header selectedCount={selectedCards.length} onAdd={onAddSelected}/>
{/*<Header selectedCount={selectedCards.length}*/}
{/* onAdd={onAddSelected}*/}
{/* title={dashboardId ? (isLibrary ? "Add Card" : "Create Card") : "Select a template to create a card"}*/}
{/*/>*/}
<Space className="items-center justify-between">
<div className="text-lg leading-4 font-semibold">
{dashboardId ? (isLibrary ? "Add Card" : "Create Card") : "Select a template to create a card"}
</div>
{isLibrary && (
<Space>
{selectedCards.length > 0 ? (
<Button type="primary" onClick={onAddSelected}>
Add {selectedCards.length} Selected
</Button>
) : ''}
<Input.Search
placeholder="Search"
// onSearch={(value) => setLibraryQuery(value)}
onChange={(value) => setLibraryQuery(value.target.value)}
style={{width: 200}}
/>
</Space>
)}
</Space>
{!isLibrary && <CategorySelector setSelected={setSelected}/>}
{isLibrary ? <CardsLibrary selectedList={selectedCards} category={selected} onCard={onCardClick}/> :
{isLibrary ? <CardsLibrary query={libraryQuery} selectedList={selectedCards} category={selected}
onCard={onCardClick}/> :
<ExampleCardsGrid items={cardItems}/>}
</>
);
};
interface HeaderProps {
selectedCount?: number,
onAdd?: () => void;
}
const Header: React.FC<HeaderProps> = ({selectedCount = 0, onAdd = () => null}) => (
<div className="flex items-center justify-between">
<div className="text-2xl leading-4 font-semibold">
Select your first card type to add to the dashboard
</div>
<div className="text-sm text-gray-500">
{selectedCount > 0 ? (
<Button type="link" onClick={onAdd}>
Add {selectedCount} Selected
</Button>
) : ''}
</div>
</div>
);
// interface HeaderProps {
// selectedCount?: number,
// onAdd?: () => void;
// title?: string;
// }
//
// const Header: React.FC<HeaderProps> = ({title = '', selectedCount = 0, onAdd = () => null}) => (
// <div className="flex items-center justify-between">
// <div className="text-lg leading-4 font-semibold">{title}</div>
// <div className="text-sm text-gray-500">
// {selectedCount > 0 ? (
// <Button type="link" onClick={onAdd}>
// Add {selectedCount} Selected
// </Button>
// ) : ''}
// </div>
// </div>
// );
interface CategorySelectorProps {
setSelected: React.Dispatch<React.SetStateAction<string>>;

View file

@ -1,7 +1,7 @@
import { useObserver } from 'mobx-react-lite';
import {useObserver} from 'mobx-react-lite';
import React from 'react';
import { Button, Modal, Form, Icon } from 'UI';
import { useStore } from 'App/mstore'
import {Button, Modal, Form, Icon} from 'UI';
import {useStore} from 'App/mstore'
import Select from 'Shared/Select';
interface Props {
@ -9,9 +9,10 @@ interface Props {
show: boolean;
closeHandler?: () => void;
}
function DashboardSelectionModal(props: Props) {
const { show, metricId, closeHandler } = props;
const { dashboardStore } = useStore();
const {show, metricId, closeHandler} = props;
const {dashboardStore} = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id,
label: i.name,
@ -41,16 +42,16 @@ function DashboardSelectionModal(props: Props) {
}, [])
return useObserver(() => (
<Modal size="small" open={ show } onClose={closeHandler}>
<Modal size="small" open={show} onClose={closeHandler}>
<Modal.Header className="flex items-center justify-between">
<div>{ 'Add to selected dashboard' }</div>
<Icon
<div>{'Add to selected dashboard'}</div>
<Icon
role="button"
tabIndex="-1"
color="gray-dark"
size="14"
name="close"
onClick={ closeHandler }
onClick={closeHandler}
/>
</Modal.Header>
@ -60,21 +61,21 @@ function DashboardSelectionModal(props: Props) {
<Select
options={dashboardOptions}
defaultValue={dashboardOptions[0].value}
onChange={({ value }: any) => setSelectedId(value.value)}
onChange={({value}: any) => setSelectedId(value.value)}
/>
</Form.Field>
</Modal.Content>
<Modal.Footer>
<Button
variant="primary"
onClick={ onSave }
onClick={onSave}
className="float-left mr-2"
>
Add
</Button>
<Button className="mr-2" onClick={ closeHandler }>{ 'Cancel' }</Button>
<Button className="mr-2" onClick={closeHandler}>{'Cancel'}</Button>
</Modal.Footer>
</Modal>
</Modal>
));
}

View file

@ -91,7 +91,7 @@ function FilterSeries(props: Props) {
canExclude = false,
expandable = false
} = props;
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = useState(!expandable);
const {series, seriesIndex} = props;
const onUpdateFilter = (filterIndex: any, filter: any) => {

View file

@ -97,7 +97,6 @@ function WidgetChart(props: Props) {
if (!isMounted()) return;
setLoading(true);
dashboardStore.fetchMetricChartData(metric, payload, isWidget, period).then((res: any) => {
console.log('res', res)
if (isMounted()) setData(res);
}).finally(() => {
setLoading(false);

View file

@ -57,7 +57,7 @@ function WidgetView(props: Props) {
React.useEffect(() => {
if (metricId && metricId !== 'create') {
metricStore.fetch(metricId, dashboardStore.period).catch((e) => {
if (e.status === 404 || e.status === 422) {
if (e.response.status === 404 || e.response.status === 422) {
setMetricNotFound(true);
}
});

View file

@ -5,7 +5,10 @@ import {useStore} from "App/mstore";
import {useObserver} from "mobx-react-lite";
import AddToDashboardButton from "Components/Dashboard/components/AddToDashboardButton";
import WidgetDateRange from "Components/Dashboard/components/WidgetDateRange/WidgetDateRange";
import {Button, Space} from "antd";
import {Button, Dropdown, MenuProps, Space, message, Modal} from "antd";
import {BellIcon, EllipsisVertical, TrashIcon} from "lucide-react";
import {useHistory} from "react-router";
import {toast} from "react-toastify";
interface Props {
onClick?: () => void;
@ -31,16 +34,69 @@ function WidgetViewHeader({onClick, onSave, undoChanges}: Props) {
<WidgetDateRange label=""/>
<AddToDashboardButton metricId={widget.metricId}/>
<Button
type="primary"
onClick={onSave}
loading={metricStore.isSaving}
disabled={metricStore.isSaving || !widget.hasChanged}
type="primary"
onClick={onSave}
loading={metricStore.isSaving}
disabled={metricStore.isSaving || !widget.hasChanged}
>
Update
</Button>
<CardViewMenu/>
</Space>
</div>
);
}
export default WidgetViewHeader;
const CardViewMenu = () => {
const history = useHistory();
const {dashboardStore, metricStore} = useStore();
const widget = useObserver(() => metricStore.instance);
const items: MenuProps['items'] = [
{
key: 'alert',
label: "Set Alerts",
icon: <BellIcon size={16}/>,
},
{
key: 'remove',
danger: true,
label: 'Remove',
icon: <TrashIcon size={16}/>,
},
];
const onClick: MenuProps['onClick'] = ({key}) => {
if (key === 'alert') {
message.info('Set Alerts');
} else if (key === 'remove') {
Modal.confirm({
title: 'Are you sure you want to remove this card?',
icon: null,
// content: 'Bla bla ...',
footer: (_, {OkBtn, CancelBtn}) => (
<>
<CancelBtn/>
<OkBtn/>
</>
),
onOk: () => {
metricStore.delete(widget).then(r => {
history.goBack();
}).catch(() => {
toast.error('Failed to remove card');
});
},
})
}
};
return (
<div className="flex items-center justify-between">
<Dropdown menu={{items, onClick}}>
<Button icon={<EllipsisVertical size={16}/>}/>
</Dropdown>
</div>
);
};

View file

@ -1,26 +1,23 @@
import React from 'react';
import WidgetIcon from './WidgetIcon';
import { useStore } from 'App/mstore';
import {useStore} from 'App/mstore';
import {Button} from "antd";
import {BellIcon} from "lucide-react";
interface Props {
seriesId: string;
initAlert?: Function;
}
function AlertButton(props: Props) {
const { seriesId } = props;
const { dashboardStore, alertsStore } = useStore();
const {seriesId} = props;
const {dashboardStore, alertsStore} = useStore();
const onClick = () => {
dashboardStore.toggleAlertModal(true);
alertsStore.init({ query: { left: seriesId }})
alertsStore.init({query: {left: seriesId}})
}
return (
<div onClick={onClick}>
<WidgetIcon
className="cursor-pointer"
icon="bell-plus"
tooltip="Set Alert"
/>
</div>
<Button onClick={onClick} type="text" icon={<BellIcon size={16}/>}/>
);
}

View file

@ -0,0 +1,48 @@
import React from 'react';
import {useHistory} from "react-router";
import {useStore} from "App/mstore";
import {useObserver} from "mobx-react-lite";
import {Button, Dropdown, MenuProps, message, Modal} from "antd";
import {BellIcon, EllipsisVertical, EyeOffIcon, PencilIcon, TrashIcon} from "lucide-react";
import {toast} from "react-toastify";
import {dashboardMetricDetails, withSiteId} from "App/routes";
function CardMenu({card}: any) {
const siteId = location.pathname.split('/')[1];
const history = useHistory();
const {dashboardStore, metricStore} = useStore();
const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
const items: MenuProps['items'] = [
{
key: 'edit',
label: "Edit",
icon: <PencilIcon size={16}/>,
},
{
key: 'hide',
label: 'Hide',
icon: <EyeOffIcon size={16}/>,
},
];
const onClick: MenuProps['onClick'] = ({key}) => {
if (key === 'edit') {
history.push(
withSiteId(dashboardMetricDetails(dashboardId, card.metricId), siteId)
)
} else if (key === 'hide') {
dashboardStore.deleteDashboardWidget(dashboardId!, card.widgetId).then(r => null);
}
};
return (
<div className="flex items-center justify-between">
<Dropdown menu={{items, onClick}} overlayStyle={{minWidth: '120px'}}>
<Button type="text" icon={<EllipsisVertical size={16}/>}/>
</Dropdown>
</div>
);
}
export default CardMenu;

View file

@ -1,7 +1,6 @@
import React, {useRef} from 'react';
import cn from 'classnames';
import {Card, Tooltip, Button} from 'antd';
import {ItemMenu, TextEllipsis} from 'UI';
import {useDrag, useDrop} from 'react-dnd';
import WidgetChart from '../WidgetChart';
import {observer} from 'mobx-react-lite';
@ -9,11 +8,12 @@ import {useStore} from 'App/mstore';
import {withRouter, RouteComponentProps} from 'react-router-dom';
import {withSiteId, dashboardMetricDetails} from 'App/routes';
import TemplateOverlay from './TemplateOverlay';
import AlertButton from './AlertButton';
import stl from './widgetWrapper.module.css';
import {FilterKey} from 'App/types/filter/filterType';
import LazyLoad from 'react-lazyload';
import {TIMESERIES} from "App/constants/card";
import CardMenu from "Components/Dashboard/components/WidgetWrapper/CardMenu";
import AlertButton from "Components/Dashboard/components/WidgetWrapper/AlertButton";
interface Props {
className?: string;
@ -74,10 +74,6 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
}),
});
const onDelete = async () => {
dashboardStore.deleteDashboardWidget(dashboard?.dashboardId!, widget.widgetId);
};
const onChartClick = () => {
if (!isWidget || isPredefined) return;
props.history.push(
@ -114,29 +110,11 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
extra={isWidget ? [
<div className="flex items-center" id="no-print">
{!isPredefined && isTimeSeries && !isGridView && (
<>
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId}/>
<div className="mx-2"/>
</>
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId}/>
)}
{!isTemplate && !isGridView && (
<ItemMenu
items={[
{
text:
widget.metricType === 'predefined'
? 'Cannot edit system generated metrics'
: 'Edit',
onClick: onChartClick,
disabled: widget.metricType === 'predefined',
},
{
text: 'Hide',
onClick: onDelete,
},
]}
/>
<CardMenu card={widget} key="card-menu"/>
)}
</div>
] : []}

View file

@ -295,8 +295,6 @@ export default class Widget {
} else if (this.metricType === TABLE) {
const totalSessions = data[0]['totalSessions'];
_data[0]['values'] = data[0]['values'].map((s: any) => new SessionsByRow().fromJson(s, totalSessions, this.metricOf));
console.log('_data[\'values\']', _data['values'])
} else {
if (data.hasOwnProperty('chart')) {
_data['value'] = data.value;