feat(ui) - dashboard - wip

This commit is contained in:
Shekar Siri 2022-03-30 18:08:09 +02:00
parent 3ae300f73e
commit 7796ff8a6c
28 changed files with 713 additions and 242 deletions

View file

@ -26,6 +26,7 @@ import { fetchList as fetchSiteList } from 'Duck/site';
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
import { fetchList as fetchAlerts } from 'Duck/alerts';
import { fetchWatchdogStatus } from 'Duck/watchdogs';
import { dashboardService } from "App/services";
import APIClient from './api_client';
import * as routes from './routes';
@ -114,6 +115,7 @@ class Router extends React.Component {
fetchInitialData = () => {
Promise.all([
this.props.fetchUserInfo().then(() => {
dashboardService.initClient();
this.props.fetchIntegrationVariables()
}),
this.props.fetchSiteList().then(() => {

View file

@ -23,6 +23,8 @@ const siteIdRequiredPaths = [
'/assist',
'/heatmaps',
'/custom_metrics',
'/dashboards',
'/metrics'
// '/custom_metrics/sessions',
];

View file

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { Switch, Route, Redirect } from 'react-router';
import withPageTitle from 'HOCs/withPageTitle';
import { observer } from "mobx-react-lite";
import { useDashboardStore } from './store/store';
import { useStore } from 'App/mstore';
import { withRouter } from 'react-router-dom';
import DashboardView from './components/DashboardView';
import {
@ -18,71 +18,67 @@ import MetricsView from './components/MetricsView';
function NewDashboard(props) {
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const store: any = useDashboardStore();
const dashboard = store.selectedDashboard;
const { dashboardStore } = useStore();
const dashboard: any = dashboardStore.selectedDashboard;
useEffect(() => {
store.setSiteId(siteId);
dashboardStore.fetchList();
dashboardStore.setSiteId(siteId);
}, []);
useEffect(() => {
if (dashboardId) {
store.selectDashboardById(dashboardId);
dashboardStore.selectDashboardById(dashboardId);
}
if (!dashboardId) {
if (dashboardId) {
store.selectDashboardById(dashboardId);
dashboardStore.selectDashboardById(dashboardId);
} else {
store.selectDefaultDashboard().then((resp) => {
dashboardStore.selectDefaultDashboard().then((resp: any) => {
history.push(withSiteId(dashboardSelected(resp.dashboardId), siteId));
});
}
}
}, []);
// console.log('rendering dashboard', props.match.params);
return (
<>
{/* { dashboard && dashboard.dashboardId && ( */}
<Switch>
<Route exact strict path={withSiteId(dashboardMetrics(), siteId)}>
<Switch>
<Route exact strict path={withSiteId(dashboardMetrics(), siteId)}>
<div className="page-margin container-90">
<div className="side-menu">
<DashboardSideMenu />
</div>
<div className="side-menu-margined">
<MetricsView />
</div>
</div>
</Route>
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}>
<WidgetView />
</Route>
{ dashboardId && (
<>
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<div className="page-margin container-90">
<div className="side-menu">
<DashboardSideMenu />
</div>
<div className="side-menu-margined">
<MetricsView />
<DashboardView dashboard={dashboard} />
</div>
</div>
</Route>
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}>
{/*
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboard.dashboardId, metricId), siteId)}>
<WidgetView />
</Route>
{ dashboardId && (
<>
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<div className="page-margin container-90">
<div className="side-menu">
<DashboardSideMenu />
</div>
<div className="side-menu-margined">
<DashboardView dashboard={dashboard} />
</div>
</div>
</Route>
{/*
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboard.dashboardId, metricId), siteId)}>
<WidgetView />
</Route> */}
{/* <Redirect exact strict to={withSiteId(dashboardSelected(dashboardId), siteId )} /> */}
</>
)}
</Switch>
{/* )} */}
</>
</Route> */}
{/* <Redirect exact strict to={withSiteId(dashboardSelected(dashboardId), siteId )} /> */}
</>
)}
</Switch>
);
}

View file

@ -0,0 +1,39 @@
import React from 'react';
import Modal from 'react-modal';
import { useStore } from 'App/mstore';
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
function DashbaordListModal(props) {
const { dashboardStore } = useStore();
const dashboards = dashboardStore.dashboards;
const activeDashboardId = dashboardStore.selectedDashboard?.dashboardId;
return (
<div className="bg-white h-screen" style={{ width: '300px'}}>
<div className="color-gray-medium uppercase p-4 text-lg">Dashboards</div>
<div>
{dashboards.map((item: any) => (
// <div className="px-4 py-3 hover:bg-gray-lightest cursor-pointer">
// {item.name}
// </div>
<div key={ item.dashboardId } className="px-4">
<SideMenuitem
key={ item.dashboardId }
active={item.dashboardId === activeDashboardId}
title={ item.name }
iconName={ item.icon }
// onClick={() => onItemClick(item)}
leading = {(
<div className="ml-2 flex items-center">
<div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>
{item.isPinned && <div className="p-1"><Icon name="pin-fill" size="16" /></div>}
</div>
)}
/>
</div>
))}
</div>
</div>
);
}
export default DashbaordListModal;

View file

@ -0,0 +1 @@
export { default } from './DashbaordListModal'

View file

@ -3,13 +3,14 @@ import React from 'react';
import { Input } from 'UI';
import { useDashboardStore } from '../../store/store';
import cn from 'classnames';
import { useStore } from 'App/mstore';
interface Props {
}
function DashboardForm(props) {
const store: any = useDashboardStore();
const dashboard = store.newDashboard;
function DashboardForm(props: Props) {
const { dashboardStore } = useStore();
const dashboard = dashboardStore.dashboardInstance;
const write = ({ target: { value, name } }) => dashboard.update({ [ name ]: value })
const writeRadio = ({ target: { value, name } }) => {

View file

@ -4,6 +4,7 @@ import { useDashboardStore } from '../../store/store';
import { useObserver } from 'mobx-react-lite';
import cn from 'classnames';
import { Button } from 'UI';
import { useStore } from 'App/mstore';
function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, unSelectCategory }) {
const selectedCategoryWidgetsCount = useObserver(() => {
@ -27,9 +28,9 @@ function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds,
}
function DashboardMetricSelection(props) {
const store: any = useDashboardStore();
const widgetCategories = store?.widgetCategories;
const widgetTemplates = store?.widgetTemplates;
const { dashboardStore } = useStore();
const widgetCategories = dashboardStore?.widgetCategories;
const widgetTemplates = dashboardStore?.widgetTemplates;
const [activeCategory, setActiveCategory] = React.useState<any>(widgetCategories[0]);
const [selectedWidgets, setSelectedWidgets] = React.useState<any>([]);
const selectedWidgetIds = selectedWidgets.map((widget: any) => widget.widgetId);
@ -73,18 +74,22 @@ function DashboardMetricSelection(props) {
</div>
<div className="col-span-9 flex items-center">
<div className="flex items-center">
<h2 className="text-2xl">Errors Tracking</h2>
<span className="text-2xl color-gray-medium ml-2">12</span>
</div>
{activeCategory && (
<>
<div className="flex items-baseline">
<h2 className="text-2xl">{activeCategory.name}</h2>
<span className="text-2xl color-gray-medium ml-2">{activeCategory.widgets.length}</span>
</div>
<div className="ml-auto flex items-center">
<span className="color-gray-medium">Showing past 7 days data for visual clue</span>
<div className="flex items-center ml-3">
<input type="checkbox" onChange={toggleAllWidgets} />
<span className="ml-2">Select All</span>
</div>
</div>
<div className="ml-auto flex items-center">
<span className="color-gray-medium">Showing past 7 days data for visual clue</span>
<div className="flex items-center ml-3">
<input type="checkbox" onChange={toggleAllWidgets} />
<span className="ml-2">Select All</span>
</div>
</div>
</>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4">
@ -104,7 +109,7 @@ function DashboardMetricSelection(props) {
</div>
<div className="col-span-9">
<div className="grid grid-cols-2 gap-4 -mx-4 px-4 lg:grid-cols-2 sm:grid-cols-1">
{activeCategory.widgets.map((widget: any) => (
{activeCategory && activeCategory.widgets.map((widget: any) => (
<div
key={widget.widgetId}
className={cn("border rounded cursor-pointer", { 'border-color-teal' : selectedWidgetIds.includes(widget.widgetId) })}

View file

@ -1,17 +1,27 @@
import React from 'react';
import { useDashboardStore } from '../../store/store';
import { useObserver } from 'mobx-react-lite';
import DashboardMetricSelection from '../DashboardMetricSelection';
import DashboardForm from '../DashboardForm';
import { Button } from 'UI';
import { useStore } from 'App/mstore';
import { useModal } from 'App/components/Modal';
function DashboardModal(props) {
const store: any = useDashboardStore();
const dashbaord = useObserver(() => store.newDashboard);
const { dashboardStore } = useStore();
const { hideModal } = useModal();
const dashbaord = useObserver(() => dashboardStore.dashboardInstance);
const loading = useObserver(() => dashboardStore.isSaving);
const onSave = () => {
dashboardStore.save(dashbaord).then(hideModal)
}
return useObserver(() => (
<div className="fixed border-r shadow p-4 h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: '9999', width: '80%'}}>
<div
className="fixed border-r shadow p-4 h-screen"
style={{ backgroundColor: '#FAFAFA', zIndex: '9999', width: '80%'}}
>
<div className="mb-6">
<h1 className="text-2xl">Create Dashboard</h1>
</div>
@ -20,7 +30,12 @@ function DashboardModal(props) {
<DashboardMetricSelection />
<div className="flex absolute bottom-0 left-0 right-0 bg-white border-t p-3">
<Button primary className="" disabled={!dashbaord.isValid} onClick={() => store.save(dashbaord)}>
<Button
primary
className=""
disabled={!dashbaord.isValid || loading}
onClick={onSave}
>
Add Selected to Dashboard
</Button>
</div>

View file

@ -1,35 +1,51 @@
import { useObserver, observer, useLocalObservable } from 'mobx-react-lite';
import React from 'react';
import { SideMenuitem, SideMenuHeader, Icon } from 'UI';
import { withDashboardStore } from '../../store/store';
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
import { useStore } from 'App/mstore';
import { withRouter } from 'react-router-dom';
import { withSiteId, dashboardSelected, dashboardMetrics } from 'App/routes';
import { useModal } from 'App/components/Modal';
import DashbaordListModal from '../DashbaordListModal';
import DashboardModal from '../DashboardModal';
const SHOW_COUNT = 5;
function DashboardSideMenu(props) {
const { store, history } = props;
const { dashboardId } = store.selectedDashboard;
const { hideModal, showModal } = useModal();
const { history } = props;
const { dashboardStore } = useStore();
const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
const dashboardsPicked = dashboardStore.dashboards.slice(0, SHOW_COUNT);
const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT;
// React.useEffect(() => {
// showModal(<DashbaordListModal />, {});
// }, []);
const redirect = (path) => {
history.push(path);
}
const onItemClick = (dashboard) => {
store.selectDashboardById(dashboard.dashboardId);
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(store.siteId));
dashboardStore.selectDashboardById(dashboard.dashboardId);
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(dashboardStore.siteId));
history.push(path);
};
const onAddDashboardClick = (e) => {
dashboardStore.initDashboard();
showModal(<DashboardModal />, {})
}
return (
<div>
<SideMenuHeader className="mb-4" text="Dashboards" />
{store.dashboards.map(item => (
{dashboardsPicked.map((item: any) => (
<SideMenuitem
key={ item.dashboardId }
active={item.dashboardId === dashboardId}
title={ item.name }
iconName={ item.icon }
onClick={() => onItemClick(item)}
leading = {(
<div className="ml-2 flex items-center">
<div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>
@ -38,13 +54,32 @@ function DashboardSideMenu(props) {
)}
/>
))}
<div>
{remainingDashboardsCount > 0 && (
<div
className="my-2 py-2 color-teal cursor-pointer"
onClick={() => showModal(<DashbaordListModal />, {})}
>
{remainingDashboardsCount} More
</div>
)}
</div>
<div className="border-t w-full my-2" />
<div className="w-full">
<SideMenuitem
id="menu-manage-alerts"
title="Create Dashboard"
iconName="plus"
onClick={onAddDashboardClick}
/>
</div>
<div className="border-t w-full my-2" />
<div className="w-full">
<SideMenuitem
id="menu-manage-alerts"
title="Metrics"
iconName="bar-chart-line"
onClick={() => redirect(withSiteId(dashboardMetrics(), store.siteId))}
onClick={() => redirect(withSiteId(dashboardMetrics(), dashboardStore.siteId))}
/>
</div>
<div className="border-t w-full my-2" />
@ -60,4 +95,4 @@ function DashboardSideMenu(props) {
);
}
export default withDashboardStore(withRouter(observer(DashboardSideMenu)));
export default withRouter(observer(DashboardSideMenu));

View file

@ -1,27 +1,24 @@
import React, { useEffect } from 'react';
import WidgetWrapper from '../../WidgetWrapper';
import React from 'react';
import { observer } from 'mobx-react-lite';
import { withDashboardStore } from '../../store/store';
import { useStore } from 'App/mstore';
import { Button, PageTitle, Link } from 'UI';
import { withSiteId, dashboardMetricCreate } from 'App/routes';
import withModal from 'App/components/Modal/withModal';
import DashboardModal from '../DashboardModal'
import DashboardWidgetGrid from '../DashboardWidgetGrid';
function DashboardView(props) {
// let { handleModal } = React.useContext(ModalContext);
const { store } = props;
const dashboard = store.selectedDashboard
const list = dashboard?.widgets;
useEffect(() => {
// props.showModal(DashboardModal)
}, [])
interface Props {
}
function DashboardView(props: Props) {
const { dashboardStore } = useStore();
const dashboard: any = dashboardStore.selectedDashboard
return (
<div>
<div className="flex items-center mb-4 justify-between">
<div className="flex items-center">
<PageTitle title={dashboard.name} className="mr-3" />
<Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), store.siteId)}><Button primary size="small">Add Metric</Button></Link>
<Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), dashboardStore.siteId)}><Button primary size="small">Add Metric</Button></Link>
</div>
<div>
Right
@ -32,4 +29,4 @@ function DashboardView(props) {
)
}
export default withDashboardStore(withModal(observer(DashboardView)));
export default withModal(observer(DashboardView));

View file

@ -1,15 +1,15 @@
import React from 'react';
import { useDashboardStore } from '../../store/store';
import { useStore } from 'App/mstore';
import WidgetWrapper from '../../WidgetWrapper';
import { NoContent, Button, Loader } from 'UI';
import { useObserver } from 'mobx-react-lite';
// import { divider } from '../../Filters/filters.css';
function DashboardWidgetGrid(props) {
const store: any = useDashboardStore();
const loading = store.isLoading;
const dashbaord = store.selectedDashboard;
const list = dashbaord.widgets;
const { dashboardStore } = useStore();
const loading = useObserver(() => dashboardStore.isLoading);
const dashbaord: any = dashboardStore.selectedDashboard;
const list: any = dashbaord?.widgets;
return useObserver(() => (
<Loader loading={loading}>
<NoContent

View file

@ -1,22 +1,22 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { Icon, NoContent, Label, Link, Pagination } from 'UI';
import { useDashboardStore } from '../../store/store';
import { useStore } from 'App/mstore';
import { getRE } from 'App/utils';
interface Props { }
function MetricsList(props: Props) {
const store: any = useDashboardStore();
const widgets = store.widgets;
const { dashboardStore } = useStore();
const widgets = dashboardStore.widgets;
const lenth = widgets.length;
const currentPage = useObserver(() => store.metricsPage);
const metricsSearch = useObserver(() => store.metricsSearch);
const currentPage = useObserver(() => dashboardStore.metricsPage);
const metricsSearch = useObserver(() => dashboardStore.metricsSearch);
const filterRE = getRE(metricsSearch, 'i');
const list = widgets.filter(w => filterRE.test(w.name))
const totalPages = list.length;
const pageSize = store.metricsPageSize;
const pageSize = dashboardStore.metricsPageSize;
const start = (currentPage - 1) * pageSize;
const end = currentPage * pageSize;
@ -64,7 +64,7 @@ function MetricsList(props: Props) {
<Pagination
page={currentPage}
totalPages={Math.ceil(totalPages / pageSize)}
onPageChange={(page) => store.updateKey('metricsPage', page)}
onPageChange={(page) => dashboardStore.updateKey('metricsPage', page)}
limit={pageSize}
debounceRequest={100}
/>

View file

@ -1,11 +1,11 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { useDashboardStore } from '../../store/store';
import { useStore } from 'App/mstore';
import { Icon } from 'UI';
function MetricsSearch(props) {
const store: any = useDashboardStore();
const metricsSearch = useObserver(() => store.metricsSearch);
const { dashboardStore } = useStore();
const metricsSearch = useObserver(() => dashboardStore.metricsSearch);
return useObserver(() => (
@ -16,7 +16,7 @@ function MetricsSearch(props) {
name="metricsSearch"
className="bg-white p-2 border rounded w-full pl-10"
placeholder="Filter by title, type, dashboard and owner"
onChange={({ target: { name, value } }) => store.updateKey(name, value)}
onChange={({ target: { name, value } }) => dashboardStore.updateKey(name, value)}
/>
</div>
));

View file

@ -2,20 +2,21 @@ import React from 'react';
import DropdownPlain from 'Shared/DropdownPlain';
import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
import { useDashboardStore } from '../../store/store';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { HelpText, Button, Icon } from 'UI'
import FilterSeries from '../FilterSeries';
import { withRouter } from 'react-router-dom';
interface Props {
// metric: any,
// editWidget: (metric, shouldFetch?) => void
history: any;
match: any;
}
function WidgetForm(props: Props) {
// const { metric } = props;
const store: any = useDashboardStore();
const metric = store.currentWidget;
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const { dashboardStore } = useStore();
const metric: any = dashboardStore.currentWidget;
const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries');
const tableOptions = metricOf.filter(i => i.type === 'table');
@ -23,28 +24,32 @@ function WidgetForm(props: Props) {
const isTimeSeries = metric.metricType === 'timeseries';
const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions);
const write = ({ target: { value, name } }) => store.editWidget({ [ name ]: value }, false);
const write = ({ target: { value, name } }) => dashboardStore.editWidget({ [ name ]: value });
const writeOption = (e, { value, name }) => {
store.editWidget({ [ name ]: value }, false);
dashboardStore.editWidget({ [ name ]: value });
if (name === 'metricValue') {
store.editWidget({ metricValue: [value] }, false);
dashboardStore.editWidget({ metricValue: [value] });
}
if (name === 'metricOf') {
if (value === FilterKey.ISSUE) {
store.editWidget({ metricValue: ['all'] }, false);
dashboardStore.editWidget({ metricValue: ['all'] });
}
}
if (name === 'metricType') {
if (value === 'timeseries') {
store.editWidget({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }, false);
dashboardStore.editWidget({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' });
} else if (value === 'table') {
store.editWidget({ metricOf: tableOptions[0].value, viewType: 'table' }, false);
dashboardStore.editWidget({ metricOf: tableOptions[0].value, viewType: 'table' });
}
}
};
const onSave = () => {
dashboardStore.saveMetric(metric, dashboardId);
}
return useObserver(() => (
<div className="p-4">
@ -141,20 +146,30 @@ function WidgetForm(props: Props) {
</div>
<div className="form-groups flex items-center justify-between">
<Button primary size="small">Save</Button>
<Button
primary
size="small"
onClick={onSave}
>
Save
</Button>
<div className="flex items-center">
<Button plain size="small" className="flex items-center">
<Icon name="trash" size="14" className="mr-2" color="teal"/>
Delete
</Button>
<Button plain size="small" className="flex items-center ml-2">
<Icon name="columns-gap" size="14" className="mr-2" color="teal"/>
Add to Dashboard
</Button>
{metric.widgetId && (
<>
<Button plain size="small" className="flex items-center">
<Icon name="trash" size="14" className="mr-2" color="teal"/>
Delete
</Button>
<Button plain size="small" className="flex items-center ml-2">
<Icon name="columns-gap" size="14" className="mr-2" color="teal"/>
Add to Dashboard
</Button>
</>
)}
</div>
</div>
</div>
));
}
export default WidgetForm;
export default withRouter(WidgetForm);

View file

@ -1,7 +1,7 @@
import React from 'react';
import cn from 'classnames';
import WidgetWrapper from '../../WidgetWrapper';
import { useDashboardStore } from '../../store/store';
import { useStore } from 'App/mstore';
import { Loader, NoContent, SegmentSelection, Icon } from 'UI';
import DateRange from 'Shared/DateRange';
import { useObserver } from 'mobx-react-lite';
@ -11,8 +11,8 @@ interface Props {
}
function WidgetPreview(props: Props) {
const { className = '' } = props;
const store: any = useDashboardStore();
const metric = store.currentWidget;
const { dashboardStore } = useStore();
const metric: any = dashboardStore.currentWidget;
const isTimeSeries = metric.metricType === 'timeseries';
const isTable = metric.metricType === 'table';

View file

@ -1,7 +1,7 @@
import React from 'react';
import { NoContent } from 'UI';
import cn from 'classnames';
import { useDashboardStore } from '../../store/store';
import { useStore } from 'App/mstore';
import SessionItem from 'Shared/SessionItem';
interface Props {
@ -9,8 +9,8 @@ interface Props {
}
function WidgetSessions(props: Props) {
const { className = '' } = props;
const store: any = useDashboardStore();
const widget = store.currentWidget;
const { dashboardStore } = useStore();
const widget = dashboardStore.currentWidget;
return (
<div className={cn(className)}>

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { withRouter } from 'react-router-dom';
import { useDashboardStore } from '../../store/store';
import { useStore } from 'App/mstore';
import WidgetForm from '../WidgetForm';
import WidgetPreview from '../WidgetPreview';
import WidgetSessions from '../WidgetSessions';
@ -11,8 +11,8 @@ interface Props {
}
function WidgetView(props: Props) {
const [expanded, setExpanded] = useState(true);
const store: any = useDashboardStore();
const widget = store.currentWidget;
const { dashboardStore } = useStore();
const widget = dashboardStore.currentWidget;
return (
<div className="page-margin container-70 mb-8">
<div className="bg-white rounded border">

View file

@ -1,15 +1,39 @@
import { makeAutoObservable, observable, action, runInAction } from "mobx"
import Widget from "./widget"
// import APIClient from 'App/api_client';
import Widget, { IWidget } from "./widget"
import { dashboardService } from 'App/services'
export default class Dashboard {
export interface IDashboard {
dashboardId: any
name: string
isPublic: boolean
widgets: IWidget[]
isValid: boolean
isPinned: boolean
currentWidget: IWidget
update(data: any): void
toJson(): any
fromJson(json: any): void
validate(): void
addWidget(widget: IWidget): void
removeWidget(widgetId: string): void
updateWidget(widget: IWidget): void
getWidget(widgetId: string): void
getWidgetIndex(widgetId: string)
getWidgetByIndex(index: number): void
getWidgetCount(): void
getWidgetIndexByWidgetId(widgetId: string): void
swapWidgetPosition(positionA: number, positionB: number): void
sortWidgets(): void
}
export default class Dashboard implements IDashboard {
dashboardId: any = undefined
name: string = "New Dashboard"
isPublic: boolean = false
widgets: Widget[] = []
widgets: IWidget[] = []
isValid: boolean = false
isPinned: boolean = false
currentWidget: Widget = new Widget()
currentWidget: IWidget = new Widget()
constructor() {
makeAutoObservable(this, {
@ -57,8 +81,8 @@ export default class Dashboard {
runInAction(() => {
this.dashboardId = json.dashboardId
this.name = json.name
this.isPublic = json.isPrivate
this.widgets = json.widgets.map(w => new Widget().fromJson(w))
this.isPublic = json.isPublic
this.widgets = json.widgets ? json.widgets.map(w => new Widget().fromJson(w)) : []
})
return this
}
@ -68,7 +92,7 @@ export default class Dashboard {
return this.isValid = this.name.length > 0
}
addWidget(widget: Widget) {
addWidget(widget: IWidget) {
this.widgets.push(widget)
}
@ -76,7 +100,7 @@ export default class Dashboard {
this.widgets = this.widgets.filter(w => w.widgetId !== widgetId)
}
updateWidget(widget: Widget) {
updateWidget(widget: IWidget) {
const index = this.widgets.findIndex(w => w.widgetId === widget.widgetId)
if (index >= 0) {
this.widgets[index] = widget

View file

@ -1,20 +1,69 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import Dashboard from "./dashboard"
import Dashboard, { IDashboard } from "./dashboard"
import APIClient from 'App/api_client';
import Widget from "./widget";
export default class DashboardStore {
import Widget, { IWidget } from "./widget";
import { dashboardService } from "App/services";
export interface IDashboardSotre {
dashboards: IDashboard[]
widgetTemplates: any[]
selectedDashboard: IDashboard | null
dashboardInstance: IDashboard
siteId: any
currentWidget: Widget
widgetCategories: any[]
widgets: Widget[]
metricsPage: number
metricsPageSize: number
metricsSearch: string
isLoading: boolean
isSaving: boolean
initDashboard(dashboard?: IDashboard): void
updateKey(key: string, value: any): void
resetCurrentWidget(): void
editWidget(widget: any): void
fetchList(): void
fetch(dashboardId: string)
save(dashboard: IDashboard): Promise<any>
saveDashboardWidget(dashboard: Dashboard, widget: Widget)
delete(dashboard: IDashboard)
toJson(): void
fromJson(json: any): void
initDashboard(dashboard: IDashboard): void
addDashboard(dashboard: IDashboard): void
removeDashboard(dashboard: IDashboard): void
getDashboard(dashboardId: string): void
getDashboardIndex(dashboardId: string): number
getDashboardCount(): void
getDashboardIndexByDashboardId(dashboardId: string): number
updateDashboard(dashboard: IDashboard): void
selectDashboardById(dashboardId: string): void
setSiteId(siteId: any): void
selectDefaultDashboard(): Promise<IDashboard>
saveMetric(metric: IWidget, dashboardId?: string): Promise<any>
}
export default class DashboardStore implements IDashboardSotre {
siteId: any = null
// Dashbaord / Widgets
dashboards: Dashboard[] = []
widgetTemplates: any[] = []
selectedDashboard: Dashboard | null = new Dashboard()
newDashboard: Dashboard = new Dashboard()
isLoading: boolean = false
siteId: any = null
dashboardInstance: IDashboard = new Dashboard()
currentWidget: Widget = new Widget()
widgetCategories: any[] = []
widgets: Widget[] = []
// Metrics
metricsPage: number = 1
metricsPageSize: number = 10
metricsSearch: string = ''
// Loading states
isLoading: boolean = false
isSaving: boolean = false
private client = new APIClient()
@ -40,25 +89,15 @@ export default class DashboardStore {
// TODO remove this sample data
this.dashboards = sampleDashboards
// this.selectedDashboard = sampleDashboards[0]
// this.dashboards = sampleDashboards
// setInterval(() => {
// this.selectedDashboard?.addWidget(getRandomWidget())
// }, 3000)
// setInterval(() => {
// this.selectedDashboard?.widgets[4].update({ position: 2 })
// this.selectedDashboard?.swapWidgetPosition(2, 0)
// }, 3000)
for (let i = 0; i < 15; i++) {
const widget: any= {};
widget.widgetId = `${i}`
widget.name = `Widget ${i}`;
widget.metricType = ['timeseries', 'table'][Math.floor(Math.random() * 2)];
this.widgets.push(widget)
}
// for (let i = 0; i < 15; i++) {
// const widget: any= {};
// widget.widgetId = `${i}`
// widget.name = `Widget ${i}`;
// widget.metricType = ['timeseries', 'table'][Math.floor(Math.random() * 2)];
// this.widgets.push(widget)
// }
for (let i = 0; i < 4; i++) {
const cat: any = {
@ -79,7 +118,10 @@ export default class DashboardStore {
this.widgetCategories.push(cat)
}
}
initDashboard(dashboard: Dashboard) {
this.dashboardInstance = dashboard || new Dashboard()
}
updateKey(key: any, value: any) {
@ -90,59 +132,71 @@ export default class DashboardStore {
this.currentWidget = new Widget()
}
editWidget(widget: Widget) {
editWidget(widget: any) {
this.currentWidget.update(widget)
}
fetchList() {
this.isLoading = true
this.client.get('/dashboards')
.then(response => {
dashboardService.getDashboards()
.then((list: any) => {
runInAction(() => {
this.dashboards = list.map(d => new Dashboard().fromJson(d))
})
}).finally(() => {
runInAction(() => {
this.dashboards = response.data.map(d => new Dashboard().fromJson(d))
this.isLoading = false
})
}
)
})
}
fetch(dashboardId: string) {
this.isLoading = true
this.client.get(`/dashboards/${dashboardId}`)
.then(response => {
runInAction(() => {
this.selectedDashboard = new Dashboard().fromJson(response.data)
this.isLoading = false
})
}
)
dashboardService.getDashboard(dashboardId).then(response => {
runInAction(() => {
this.selectedDashboard = new Dashboard().fromJson(response)
})
}).finally(() => {
runInAction(() => {
this.isLoading = false
})
})
}
save(dashboard: Dashboard) {
dashboard.validate()
if (dashboard.isValid) {
this.isLoading = true
if (dashboard.dashboardId) {
this.client.put(`/dashboards/${dashboard.dashboardId}`, dashboard.toJson())
.then(response => {
runInAction(() => {
this.isLoading = false
})
}
)
} else {
this.client.post('/dashboards', dashboard.toJson())
.then(response => {
runInAction(() => {
this.isLoading = false
})
}
)
}
} else {
alert("Invalid dashboard") // TODO show validation errors
}
save(dashboard: IDashboard): Promise<any> {
this.isSaving = true
const isCreating = !dashboard.dashboardId
return dashboardService.saveDashboard(dashboard).then(response => {
runInAction(() => {
if (isCreating) {
this.addDashboard(response.data)
} else {
this.updateDashboard(response.data)
}
})
}).finally(() => {
runInAction(() => {
this.isSaving = false
})
})
}
saveMetric(metric: IWidget, dashboardId: string): Promise<any> {
const isCreating = !metric.widgetId
return dashboardService.saveMetric(metric, dashboardId).then(metric => {
runInAction(() => {
if (isCreating) {
this.selectedDashboard?.widgets.push(metric)
} else {
this.selectedDashboard?.widgets.map(w => {
if (w.widgetId === metric.widgetId) {
w.update(metric)
}
})
}
})
})
}
saveDashboardWidget(dashboard: Dashboard, widget: Widget) {
@ -193,10 +247,6 @@ export default class DashboardStore {
return this
}
initDashboard(dashboard: Dashboard | null) {
this.selectedDashboard = dashboard || new Dashboard()
}
addDashboard(dashboard: Dashboard) {
this.dashboards.push(dashboard)
}
@ -234,13 +284,16 @@ export default class DashboardStore {
selectDashboardById = (dashboardId: any) => {
this.selectedDashboard = this.dashboards.find(d => d.dashboardId == dashboardId) || new Dashboard();
if (this.selectedDashboard.dashboardId) {
this.fetch(this.selectedDashboard.dashboardId)
}
}
setSiteId = (siteId: any) => {
this.siteId = siteId
}
selectDefaultDashboard = () => {
selectDefaultDashboard = (): Promise<Dashboard> => {
return new Promise((resolve, reject) => {
if (this.dashboards.length > 0) {
const pinnedDashboard = this.dashboards.find(d => d.isPinned)
@ -250,7 +303,11 @@ export default class DashboardStore {
this.selectedDashboard = this.dashboards[0]
}
}
resolve(this.selectedDashboard)
if (this.selectedDashboard) {
resolve(this.selectedDashboard)
}
reject(new Error("No dashboards found"))
})
}
}

View file

@ -2,7 +2,36 @@ import { makeAutoObservable, runInAction, observable, action, reaction } from "m
import Filter from 'Types/filter';
import FilterSeries from "./filterSeries";
export default class Widget {
export interface IWidget {
widgetId: any
name: string
metricType: string
metricOf: string
metricValue: string
viewType: string
series: FilterSeries[]
sessions: []
isPublic: boolean
owner: string
lastModified: Date
dashboardIds: any[]
position: number
data: any
isLoading: boolean
isValid: boolean
dashboardId: any
colSpan: number
udpateKey(key: string, value: any): void
removeSeries(index: number): void
addSeries(): void
fromJson(json: any): void
toJson(): any
validate(): void
update(data: any): void
}
export default class Widget implements IWidget {
widgetId: any = undefined
name: string = "New Metric"
metricType: string = "timeseries"
@ -11,7 +40,7 @@ export default class Widget {
viewType: string = "lineChart"
series: FilterSeries[] = []
sessions: [] = []
isPrivate: boolean = false
isPublic: boolean = false
owner: string = ""
lastModified: Date = new Date()
dashboardIds: any[] = []
@ -76,7 +105,12 @@ export default class Widget {
return {
widgetId: this.widgetId,
name: this.name,
data: this.data
metricOf: this.metricOf,
metricValue: this.metricValue,
viewType: this.viewType,
series: this.series,
sessions: this.sessions,
isPublic: this.isPublic,
}
}

View file

@ -1,16 +1,15 @@
import React from 'react';
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import { useModal } from '.';
import ModalOverlay from './ModalOverlay';
export default class Modal extends React.PureComponent {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
export default function Modal({ children }){
const { component } = useModal();
render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
return component ? ReactDOM.createPortal(
<ModalOverlay>
{component}
</ModalOverlay>,
document.querySelector("#modal-root"),
) : null;
}

View file

@ -0,0 +1,32 @@
.overlay {
/* absolute w-full h-screen cursor-pointer */
position: absolute;
width: 100%;
height: 100vh;
cursor: pointer;
/* transition: all 0.3s ease-in-out; */
animation: fade 1s forwards;
}
.slide {
position: absolute;
left: -100%;
-webkit-animation: slide 0.5s forwards;
animation: slide 0.5s forwards;
}
@keyframes fade {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-webkit-keyframes slide {
100% { left: 0; }
}
@keyframes slide {
100% { left: 0; }
}

View file

@ -1,14 +1,19 @@
import React from 'react';
import { ModalContext } from "App/components/Modal/modalContext";
import useModal from 'App/components/Modal/useModal';
import { useModal } from 'App/components/Modal';
import stl from './ModalOverlay.css'
function ModalOverlay({ children }) {
let modal = useModal();
// console.log('m', m);
return (
<div onClick={() => modal.handleModal(false)} style={{ background: "rgba(0,0,0,0.8)", zIndex: '9999' }}>
{children}
<div className="fixed w-full h-screen" style={{ zIndex: '99999' }}>
<div
onClick={() => modal.hideModal()}
className={stl.overlay}
style={{ background: "rgba(0,0,0,0.5)" }}
/>
<div className={stl.slide}>{children}</div>
</div>
);
}

View file

@ -0,0 +1,44 @@
import React, { Component, createContext } from 'react';
import Modal from './Modal';
const ModalContext = createContext({
component: null,
props: {},
showModal: (component: any, props: any) => {},
hideModal: () => {}
});
export class ModalProvider extends Component {
showModal = (component, props = {}) => {
this.setState({
component,
props
});
};
hideModal = () =>
this.setState({
component: null,
props: {}
});
state = {
component: null,
props: {},
showModal: this.showModal,
hideModal: this.hideModal
};
render() {
return (
<ModalContext.Provider value={this.state}>
<Modal />
{this.props.children}
</ModalContext.Provider>
);
}
}
export const ModalConsumer = ModalContext.Consumer;
export const useModal = () => React.useContext(ModalContext);

View file

@ -5,28 +5,27 @@ import { Provider } from 'react-redux';
import store from './store';
import Router from './Router';
import DashboardStore from './components/Dashboard/store';
import { DashboardStoreProvider } from './components/Dashboard/store/store';
import { ModalProvider } from './components/Modal/ModalContext';
import { StoreProvider, RootStore } from './mstore';
import { ModalProvider } from './components/Modal';
import ModalRoot from './components/Modal/ModalRoot';
import { HTML5Backend } from 'react-dnd-html5-backend'
import { DndProvider } from 'react-dnd'
import Modal from 'react-modal';
Modal.setAppElement('#modal-root');
document.addEventListener('DOMContentLoaded', () => {
const dashboardStore = new DashboardStore();
render(
(
<Provider store={ store }>
<DndProvider backend={HTML5Backend}>
<DashboardStoreProvider store={ dashboardStore }>
<StoreProvider store={new RootStore()}>
<DndProvider backend={HTML5Backend}>
<ModalProvider>
<ModalRoot />
<Router />
<ModalRoot />
<Router />
</ModalProvider>
</DashboardStoreProvider>
</DndProvider>
</DndProvider>
</StoreProvider>
</Provider>
),
document.getElementById('app'),

View file

@ -0,0 +1,24 @@
import React from 'react';
import DashboardStore, { IDashboardSotre } from 'App/components/Dashboard/store/DashboardStore';
export class RootStore {
dashboardStore: IDashboardSotre;
constructor() {
this.dashboardStore = new DashboardStore();
}
}
const StoreContext = React.createContext<RootStore>({} as RootStore);
export const StoreProvider = ({ children, store }) => {
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
};
export const useStore = () => React.useContext(StoreContext);
export const withStore = (Component) => (props) => {
return <Component {...props} store={useStore()} />;
};

View file

@ -0,0 +1,142 @@
import { IDashboard } from "App/components/Dashboard/store/dashboard";
import APIClient from 'App/api_client';
import { IWidget } from "App/components/Dashboard/store/widget";
export interface IDashboardService {
initClient(): void
getWidgets(dashboardId: string): Promise<any>
getDashboards(): Promise<any[]>
getDashboard(dashboardId: string): Promise<any>
saveDashboard(dashboard: IDashboard): Promise<any>
deleteDashboard(dashboardId: string): Promise<any>
saveMetric(metric: IWidget, dashboardId?: string): Promise<any>
deleteMetric(metricId: string): Promise<any>
saveWidget(dashboardId: string, widget: IWidget): Promise<any>
deleteWidget(dashboardId: string, widgetId: string): Promise<any>
}
export class DashboardService implements IDashboardService {
private client: APIClient;
constructor(client?: APIClient) {
this.client = client ? client : new APIClient();
}
initClient() {
this.client = new APIClient();
}
/**
* Get all widgets from a dashboard.
* @param dashboardId Required
* @returns
*/
getWidgets(dashboardId: string): Promise<any> {
return this.client.get(`/dashboards/${dashboardId}/widgets`)
.then(response => response.json())
.then(response => response.data || []);
}
/**
* Get all dashboards.
* @returns {Promise<any>}
*/
getDashboards(): Promise<any[]> {
return this.client.get('/dashboards')
.then(response => response.json())
.then(response => response.data || []);
}
/**
* Get a dashboard by dashboardId.
* @param dashboardId
* @returns {Promise<any>}
*/
getDashboard(dashboardId: string): Promise<any> {
return this.client.get('/dashboards/' + dashboardId)
.then(response => response.json())
.then(response => response.data || {});
}
/**
* Create or update a dashboard.
* @param dashboard Required
* @returns {Promise<any>}
*/
saveDashboard(dashboard: IDashboard): Promise<any> {
const data = dashboard.toJson();
if (dashboard.dashboardId) {
return this.client.put(`/dashboards/${dashboard.dashboardId}`, data)
.then(response => response.json())
.then(response => response.data || {});
} else {
return this.client.post('/dashboards', data)
.then(response => response.json())
.then(response => response.data || {});
}
}
/**
* Delete a dashboard.
* @param dashboardId
* @returns {Promise<any>}
*/
deleteDashboard(dashboardId: string): Promise<any> {
return this.client.delete(`/dashboards/${dashboardId}`)
}
/**
* Create a new Meitrc, if the dashboardId is not provided,
* it will add the metric to the dashboard.
* @param metric Required
* @param dashboardId Optional
* @returns {Promise<any>}
*/
saveMetric(metric: IWidget, dashboardId?: string): Promise<any> {
const data = metric.toJson();
const path = dashboardId ? `/metrics` : '/metrics'; // TODO change to /dashboards/:dashboardId/widgets
// const path = dashboardId ? `/dashboards/${dashboardId}/widgets` : '/widgets';
if (metric.widgetId) {
return this.client.put(path + '/' + metric.widgetId, data)
} else {
return this.client.post(path, data)
}
}
/**
* Delete a Metric by metricId.
* @param metricId
* @returns {Promise<any>}
*/
deleteMetric(metricId: string): Promise<any> {
return this.client.delete(`/metrics/${metricId}`)
}
/**
* Remove a widget from a dashboard.
* @param dashboardId Required
* @param widgetId Required
* @returns {Promise<any>}
*/
deleteWidget(dashboardId: string, widgetId: string): Promise<any> {
return this.client.delete(`/dashboards/${dashboardId}/widgets/${widgetId}`)
}
/**
* Add a widget to a dashboard.
* @param dashboardId Required
* @param widget Required
* @returns {Promise<any>}
*/
saveWidget(dashboardId: string, widget: IWidget): Promise<any> {
return this.client.post(`/dashboards/${dashboardId}/widgets`, widget.toJson())
}
}

View file

@ -0,0 +1,3 @@
import { DashboardService, IDashboardService } from "./DashboardService";
export const dashboardService: IDashboardService = new DashboardService();