feat(ui) - dashboards wip

This commit is contained in:
Shekar Siri 2022-03-21 12:28:27 +01:00
parent 391189e894
commit 94370f028f
16 changed files with 223 additions and 90 deletions

View file

@ -48,8 +48,13 @@ const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails);
const withSiteId = routes.withSiteId;
const withObTab = routes.withObTab;
const DASHBOARD_PATH = routes.dashboardSelected();
const WIDGET_PATAH = routes.dashboardMetric();
const DASHBOARD_PATH = routes.dashboard();
// const DASHBOARD_WIDGET_CREATE_PATH = routes.dashboardMetricCreate();
// const DASHBOARD_WIDGET_DETAILS_PATH = routes.dashboardMetricDetails();
// const METRIC_CREATE_PATH = routes.metricCreate();
// const METRIC_DETAILS_PATH = routes.metricDetails();
// const WIDGET_PATAH = routes.dashboardMetric();
const SESSIONS_PATH = routes.sessions();
const ASSIST_PATH = routes.assist();
const ERRORS_PATH = routes.errors();
@ -182,8 +187,13 @@ class Router extends React.Component {
{ siteIdList.length === 0 &&
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
}
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
<Route path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
{/* <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } /> */}
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />

View file

@ -1,46 +1,63 @@
import React, { useEffect } from 'react';
import { Switch, Route, Redirect } from 'react-router';
import withPageTitle from 'HOCs/withPageTitle';
import { observer, useObserver } from "mobx-react-lite";
import { withDashboardStore } from './store/store';
import { observer } from "mobx-react-lite";
import { useDashboardStore } from './store/store';
import { withRouter } from 'react-router-dom';
import DashboardView from './components/DashboardView';
import { dashboardSelected, dashboardMetric, withSiteId } from 'App/routes';
import { dashboardSelected, dashboardMetricDetails, dashboardMetricCreate, withSiteId } from 'App/routes';
import DashboardSideMenu from './components/DashboardSideMenu';
import WidgetView from './WidgetView';
function NewDashboard(props) {
const { store, match: { params: { siteId, dashboardId, metricId } } } = props;
const { match: { params: { siteId, dashboardId, metricId } } } = props;
const store: any = useDashboardStore();
const dashboard = store.selectedDashboard;
useEffect(() => {
store.setSiteId(siteId);
if (dashboardId) {
store.selectDashboardById(dashboardId);
} else {
store.selectDefaultDashboard();
}
}, [dashboardId]);
}, []);
return useObserver(() => (
useEffect(() => {
console.log('dashboardId', dashboardId);
if (!dashboard || !dashboard.dashboardId) {
if (dashboardId) {
store.selectDashboardById(dashboardId);
} else {
store.selectDefaultDashboard();
}
}
}, [dashboard]);
return (
<div className="page-margin container-90">
<div className="side-menu">
<DashboardSideMenu />
</div>
<div className="side-menu-margined">
<Switch>
<Route exact path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<DashboardView dashboard={dashboard} />
</Route>
<Route exact path={withSiteId(dashboardMetric(dashboardId, metricId), siteId)}>
<h1>Metric</h1>
</Route>
<Redirect to={withSiteId(dashboardSelected(dashboardId), siteId )} />
</Switch>
{ dashboard && dashboard.dashboardId && (
<Switch>
<Route exact strict path={withSiteId(dashboardSelected(dashboard.dashboardId), siteId)}>
<DashboardView dashboard={dashboard} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}>
<WidgetView />
</Route>
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboard.dashboardId, metricId), siteId)}>
<WidgetView />
</Route>
{/* <Route exact strict path={withSiteId((dashboard.dashboardId), siteId)}>
<WidgetView />
</Route> */}
<Redirect exact strict to={withSiteId(dashboardSelected(dashboard.dashboardId), siteId )} />
</Switch>
)}
</div>
</div>
));
);
}
export default withPageTitle('New Dashboard')(
withRouter(withDashboardStore(NewDashboard))
withRouter(observer(NewDashboard))
);

View file

@ -22,7 +22,7 @@ function SideMenuSection({ title, items, onItemClick, setShowAlerts, siteId }) {
)}
<div className={stl.divider} />
<div className="my-3">
<div className="my-3">
<SideMenuitem
id="menu-manage-alerts"
title="Manage Alerts"

View file

@ -1,11 +1,20 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import { useDashboardStore } from '../store/store';
function WidgetView(props) {
console.log('WidgetView', props);
const store: any = useDashboardStore();
const widget = store.currentWidget;
return (
<div>
Widget view
<div className="bg-white rounded border">
<div className="p-3">
<h1 className="mb-0 text-2xl">{widget.name}</h1>
</div>
</div>
</div>
);
}
export default WidgetView;
export default withRouter(WidgetView);

View file

@ -1,9 +1,7 @@
import React from 'react';
import { observer } from "mobx-react-lite";
import { useDashboardStore } from '../store/store';
import cn from 'classnames';
import { Link } from 'UI';
import { dashboardMetric, withSiteId } from 'App/routes';
import { ItemMenu } from 'UI';
function WidgetWrapper(props) {
const { widget } = props;
@ -12,23 +10,33 @@ function WidgetWrapper(props) {
const siteId = store.siteId;
return (
<div className={cn("border rounded", 'col-span-' + widget.colSpan)} style={{ userSelect: 'none'}}>
<Link to={withSiteId(dashboardMetric(12, widget.widgetId), siteId)}>
<div className="p-3 cursor-pointer bg-white border-b flex items-center justify-between">
<div className={cn("border rounded bg-white", 'col-span-' + widget.colSpan)} style={{ userSelect: 'none'}}>
{/* <Link to={withSiteId(dashboardMetricDetails(dashboard.dashboardId, widget.widgetId), siteId)}> */}
<div className="p-3 cursor-pointe border-b flex items-center justify-between">
{widget.name} - {widget.position}
<div>
<button className="btn btn-sm btn-outline-primary" onClick={() => dashboard.removeWidget(widget.widgetId)}>
<ItemMenu
items={[
{
text: 'Edit',
onClick: () => {
console.log('edit');
}
},
]}
/>
{/* <button className="btn btn-sm btn-outline-primary" onClick={() => dashboard.removeWidget(widget.widgetId)}>
remove
</button>
</button> */}
</div>
</div>
<div className="bg-white h-40">
<div className="h-40">
</div>
</Link>
{/* </Link> */}
</div>
);
}
export default observer(WidgetWrapper);
export default WidgetWrapper;

View file

@ -1,39 +1,51 @@
import { useObserver } from 'mobx-react-lite';
import { useObserver, observer, useLocalObservable } from 'mobx-react-lite';
import React from 'react';
import { SideMenuitem, SideMenuHeader } from 'UI';
import { SideMenuitem, SideMenuHeader, Icon } from 'UI';
import { withDashboardStore } from '../../store/store';
import { withRouter } from 'react-router-dom';
import { withSiteId, dashboardSelected } from 'App/routes';
function DashboardSideMenu(props) {
const { store } = props;
const { selectedDashboard } = store;
const { store, history } = props;
const { dashboardId } = store.selectedDashboard;
const onItemClick = (dashboard) => {
store.selectDashboardById(dashboard.dashboardId);
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(store.siteId));
// console.log('path', path);
// history.push(path);
};
return useObserver(() => (
return (
<div>
<SideMenuHeader className="mb-4" text="Dashboards" />
{store.dashboards.map(item => (
<SideMenuitem
key={ item.key }
active={ item.active }
key={ item.key }
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>
{item.isPinned && <div className="p-1"><Icon name="pin-fill" size="16" /></div>}
</div>
)}
/>
))}
<div className="border-t w-full my-2" />
<div className="">
<div className="w-full">
<SideMenuitem
id="menu-manage-alerts"
title="Metrics"
iconName="bar-chart-line"
// onClick={() => setShowAlerts(true)}
/>
/>
</div>
<div className="border-t w-full my-2" />
<div className="my-3">
<div className="my-3 w-full">
<SideMenuitem
id="menu-manage-alerts"
title="Alerts"
@ -42,7 +54,7 @@ function DashboardSideMenu(props) {
/>
</div>
</div>
));
);
}
export default withDashboardStore(DashboardSideMenu);
export default withDashboardStore(withRouter(observer(DashboardSideMenu)));

View file

@ -2,7 +2,8 @@ import React from 'react';
import WidgetWrapper from '../../WidgetWrapper';
import { observer } from 'mobx-react-lite';
import { withDashboardStore } from '../../store/store';
import { Button, PageTitle } from 'UI';
import { Button, PageTitle, Link } from 'UI';
import { withSiteId, dashboardMetricCreate } from 'App/routes';
function DashboardView(props) {
const { store } = props;
@ -12,7 +13,7 @@ function DashboardView(props) {
<div>
<div className="flex items-center mb-4">
<PageTitle title={dashboard.name} className="mr-3" />
<Button primary size="small">Add Metric</Button>
<Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), store.siteId)}><Button primary size="small">Add Metric</Button></Link>
</div>
<div className="grid grid-cols-2 gap-4">
{list && list.map(item => <WidgetWrapper widget={item} key={item.widgetId} />)}

View file

@ -1,4 +1,4 @@
import { makeAutoObservable, makeObservable, observable, action, runInAction, computed, reaction } from "mobx"
import { makeAutoObservable, observable, action, runInAction } from "mobx"
import Widget from "./widget"
// import APIClient from 'App/api_client';
@ -9,6 +9,7 @@ export default class Dashboard {
widgets: Widget[] = []
isValid: boolean = false
isPinned: boolean = false
currentWidget: Widget = new Widget()
constructor() {
makeAutoObservable(this, {

View file

@ -8,6 +8,7 @@ export default class DashboardStore {
selectedDashboard: Dashboard | null = new Dashboard()
isLoading: boolean = false
siteId: any = null
currentWidget: Widget = new Widget()
private client = new APIClient()
@ -25,7 +26,8 @@ export default class DashboardStore {
getDashboardByIndex: action,
getDashboardCount: action,
getDashboardIndexByDashboardId: action,
selectDashboardById: action,
selectDashboardById: action,
selectDefaultDashboard: action,
toJson: action,
fromJson: action,
setSiteId: action,
@ -34,7 +36,7 @@ export default class DashboardStore {
// TODO remove this sample data
this.dashboards = sampleDashboards
this.selectedDashboard = sampleDashboards[0]
// this.selectedDashboard = sampleDashboards[0]
// setInterval(() => {
// this.selectedDashboard?.addWidget(getRandomWidget())
@ -185,7 +187,7 @@ export default class DashboardStore {
}
selectDashboardById = (dashboardId: any) => {
this.selectedDashboard = this.dashboards.find(d => d.dashboardId == dashboardId) || null;;
this.selectedDashboard = this.dashboards.find(d => d.dashboardId == dashboardId) || new Dashboard();
}
setSiteId = (siteId: any) => {
@ -193,15 +195,13 @@ export default class DashboardStore {
}
selectDefaultDashboard = () => {
const pinnedDashboard = this.dashboards.find(d => d.isPinned)
if (this.dashboards.length > 0) {
const pinnedDashboard = this.dashboards.find(d => d.isPinned)
if (pinnedDashboard) {
this.selectedDashboard = pinnedDashboard
} else {
this.selectedDashboard = this.dashboards[0]
}
} else {
this.selectedDashboard = new Dashboard()
}
}
}
@ -209,17 +209,38 @@ export default class DashboardStore {
function getRandomWidget() {
const widget = new Widget();
widget.widgetId = Math.floor(Math.random() * 100);
widget.name = "Widget " + Math.floor(Math.random() * 100);
widget.name = randomMetricName();
widget.type = "random";
widget.colSpan = Math.floor(Math.random() * 2) + 1;
return widget;
}
function getRandomDashboard(id: any = null) {
function generateRandomPlaceName() {
const placeNames = [
"New York",
"Los Angeles",
"Chicago",
"Houston",
"Philadelphia",
"Phoenix",
"San Antonio",
"San Diego",
]
return placeNames[Math.floor(Math.random() * placeNames.length)]
}
function randomMetricName () {
const metrics = ["Revenue", "Profit", "Expenses", "Sales", "Orders", "Revenue", "Profit", "Expenses", "Sales", "Orders", "Revenue", "Profit", "Expenses", "Sales", "Orders", "Revenue", "Profit", "Expenses", "Sales", "Orders"];
return metrics[Math.floor(Math.random() * metrics.length)];
}
function getRandomDashboard(id: any = null, isPinned = false) {
const dashboard = new Dashboard();
dashboard.name = "Random Dashboard";
dashboard.dashboardId = id ? id : "random-dashboard-" + Math.floor(Math.random() * 10);
for (let i = 0; i < 10; i++) {
dashboard.name = generateRandomPlaceName();
dashboard.dashboardId = id ? id : Math.floor(Math.random() * 10);
dashboard.isPinned = isPinned;
for (let i = 0; i < 8; i++) {
const widget = getRandomWidget();
widget.position = i;
dashboard.addWidget(widget);
@ -228,8 +249,8 @@ function getRandomDashboard(id: any = null) {
}
const sampleDashboards = [
getRandomDashboard(12),
getRandomDashboard(),
getRandomDashboard(),
getRandomDashboard(),
getRandomDashboard(1, true),
getRandomDashboard(2),
getRandomDashboard(3),
getRandomDashboard(4),
]

View file

@ -2,7 +2,7 @@ import { makeAutoObservable, runInAction, observable, action, reaction } from "m
export default class Widget {
widgetId: any = undefined
name: string = ""
name: string = "New Metric"
type: string = ""
position: number = 0
data: any = {}

View file

@ -39,13 +39,22 @@ export default class ItemMenu extends React.PureComponent {
return (
<div className={ styles.wrapper }>
<div
{/* <div
ref={ (ref) => { this.menuBtnRef = ref; } }
className={ styles.menuBtn }
onClick={ this.toggleMenu }
role="button"
tabIndex="-1"
/>
/> */}
<div
ref={ (ref) => { this.menuBtnRef = ref; } }
className="w-10 h-10 cursor-pointer bg-white rounded-full flex items-center justify-center hover:bg-gray-lightest"
onClick={ this.toggleMenu }
role="button"
tabIndex="-1"
>
<Icon name="ellipsis-v" size="16" />
</div>
<div
className={ styles.menu }
data-displayed={ displayed }
@ -58,9 +67,11 @@ export default class ItemMenu extends React.PureComponent {
role="menuitem"
tabIndex="-1"
>
<div className={ styles.iconWrapper }>
<Icon name={ icon } size="13" color="gray-dark" />
</div>
{ icon && (
<div className={ styles.iconWrapper }>
<Icon name={ icon } size="13" color="gray-dark" />
</div>
)}
<div>{ text }</div>
</div>
))}

View file

@ -6,7 +6,7 @@
}
.menuBtn {
@mixin icon-before ellipsis-v, $gray-darkest, 25px {
@mixin icon-before ellipsis-v, $gray-darkest, 18px {
margin: 5px;
}
width: 36px;

View file

@ -3,7 +3,20 @@ import { Icon, Popup } from 'UI';
import cn from 'classnames';
import stl from './sideMenuItem.css';
function SideMenuitem({ iconBg = false, iconColor = "gray-dark", iconSize = 18, className, iconName = null, title, active = false, disabled = false, onClick, deleteHandler, ...props }) {
function SideMenuitem({
iconBg = false,
iconColor = "gray-dark",
iconSize = 18,
className,
iconName = null,
title,
active = false,
disabled = false,
onClick,
deleteHandler,
leading = null,
...props
}) {
return (
<Popup
trigger={
@ -17,14 +30,17 @@ function SideMenuitem({ iconBg = false, iconColor = "gray-dark", iconSize = 18,
onClick={disabled ? null : onClick}
{...props}
>
<div className={ cn(stl.iconLabel, 'flex items-center', { [stl.disabled] : disabled })}>
<div className={ cn('flex items-center w-full', { [stl.disabled] : disabled })}>
<div className={cn("flex items-center", stl.iconLabel)}>
{ iconName && (
<div className="flex items-center justify-center w-8 h-8 mr-2">
<div className={cn({ "w-8 h-8 rounded-full relative opacity-20" : iconBg }, iconBg)} style={{ opacity: '0.2'}} />
<Icon name={ iconName } size={ iconSize } color={active ? 'teal' : iconColor} className="absolute" />
</div>
)}
<span className={stl.title}>{ title }</span>
<div className="flex items-center justify-center w-8 h-8 mr-2">
<div className={cn({ "w-8 h-8 rounded-full relative opacity-20" : iconBg }, iconBg)} style={{ opacity: '0.2'}} />
<Icon name={ iconName } size={ iconSize } color={active ? 'teal' : iconColor} className="absolute" />
</div>
)}
<span className={stl.title}>{ title }</span>
</div>
{ leading && leading }
</div>
{deleteHandler &&
<div onClick={deleteHandler} className={stl.actions}><Icon name="trash" size="14" /></div>

View file

@ -5,10 +5,14 @@
cursor: pointer;
&:hover {
color: $teal;
& svg {
fill: $teal;
& .iconLabel {
color: $teal;
& svg {
fill: $teal;
}
}
& .actions {
opacity: 1;
}

View file

@ -102,14 +102,34 @@ export const testBuilder = (testId = ':testId') => `/test-builder/${ testId }`;
export const dashboard = () => '/dashboard';
export const dashboardSelected = (id = ':dashboardId', hash) => hashed(`/dashboard/${ id }`, hash);
export const dashboardMetric = (id = ':dashboardId', metricId = ':metricId', hash) => hashed(`/dashboard/${ id }/metric/${metricId}`, hash);
export const dashboardMetricDetails = (id = ':dashboardId', metricId = ':metricId', hash) => hashed(`/dashboard/${ id }/metric/${metricId}`, hash);
export const dashboardMetricCreate = (id = ':dashboardId', hash) => hashed(`/dashboard/${ id }/metric/create`, hash);
export const metricCreate = () => `/metric/create`;
export const metricDetails = (id = ':metricId', hash) => hashed(`/metric/${ id }`, hash);
export const RESULTS_QUERY_KEY = 'results';
export const METRICS_QUERY_KEY = 'metrics';
export const SOURCE_QUERY_KEY = 'source';
export const WIDGET_QUERY_KEY = 'widget';
const REQUIRED_SITE_ID_ROUTES = [ liveSession(''), session(''), sessions(), assist(), dashboard(''), error(''), errors(), onboarding(''), funnel(''), funnelIssue(''), ];
const REQUIRED_SITE_ID_ROUTES = [
liveSession(''),
session(''),
sessions(),
assist(),
dashboard(''),
dashboardSelected(''),
// dashboardMetricCreate(''),
dashboardMetricDetails(''),
metricCreate(''),
error(''),
errors(),
onboarding(''),
funnel(''),
funnelIssue(''),
];
const routeNeedsSiteId = path => REQUIRED_SITE_ID_ROUTES.some(r => path.startsWith(r));
const siteIdToUrl = (siteId = ':siteId') => {
if (Array.isArray(siteId)) {
@ -132,7 +152,7 @@ export function isRoute(route, path){
routeParts.every((p, i) => p.startsWith(':') || p === pathParts[ i ]);
}
const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), assist(), dashboard(), errors(), onboarding('')];
const SITE_CHANGE_AVALIABLE_ROUTES = [ sessions(), assist(), dashboard(), dashboardSelected(''), errors(), onboarding('')];
export const siteChangeAvaliable = path => SITE_CHANGE_AVALIABLE_ROUTES.some(r => isRoute(r, path));
export const redirects = Object.entries({

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-pin-fill" viewBox="0 0 16 16">
<path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5c0 .276-.224 1.5-.5 1.5s-.5-1.224-.5-1.5V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A5.921 5.921 0 0 1 5 6.708V2.277a2.77 2.77 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354z"/>
</svg>

After

Width:  |  Height:  |  Size: 490 B