feat(ui) - dashboard - wip

This commit is contained in:
Shekar Siri 2022-04-04 15:22:51 +02:00
parent 7796ff8a6c
commit 1549c554a7
38 changed files with 1473 additions and 280 deletions

View file

@ -1,3 +1,4 @@
import React, { lazy, Suspense } from 'react';
import { Switch, Route, Redirect } from 'react-router'; import { Switch, Route, Redirect } from 'react-router';
import { BrowserRouter, withRouter } from 'react-router-dom'; import { BrowserRouter, withRouter } from 'react-router-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -5,28 +6,29 @@ import { Notification } from 'UI';
import { Loader } from 'UI'; import { Loader } from 'UI';
import { fetchUserInfo } from 'Duck/user'; import { fetchUserInfo } from 'Duck/user';
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
import Login from 'Components/Login/Login'; const Login = lazy(() => import('Components/Login/Login'));
import ForgotPassword from 'Components/ForgotPassword/ForgotPassword'; const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword'));
import UpdatePassword from 'Components/UpdatePassword/UpdatePassword'; const UpdatePassword = lazy(() => import('Components/UpdatePassword/UpdatePassword'));
import ClientPure from 'Components/Client/Client'; const SessionPure = lazy(() => import('Components/Session/Session'));
import OnboardingPure from 'Components/Onboarding/Onboarding'; const LiveSessionPure = lazy(() => import('Components/Session/LiveSession'));
import SessionPure from 'Components/Session/Session'; const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
import LiveSessionPure from 'Components/Session/LiveSession'; const ClientPure = lazy(() => import('Components/Client/Client'));
import AssistPure from 'Components/Assist'; const AssistPure = lazy(() => import('Components/Assist'));
import BugFinderPure from 'Components/BugFinder/BugFinder'; const BugFinderPure = lazy(() => import('Components/BugFinder/BugFinder'));
import DashboardPure from 'Components/Dashboard/NewDashboard'; const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard'));
const ErrorsPure = lazy(() => import('Components/Errors/Errors'));
const FunnelDetails = lazy(() => import('Components/Funnels/FunnelDetails'));
const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDetails'));
import WidgetViewPure from 'Components/Dashboard/components/WidgetView'; import WidgetViewPure from 'Components/Dashboard/components/WidgetView';
import ErrorsPure from 'Components/Errors/Errors';
import Header from 'Components/Header/Header'; import Header from 'Components/Header/Header';
// import ResultsModal from 'Shared/Results/ResultsModal'; // import ResultsModal from 'Shared/Results/ResultsModal';
import FunnelDetails from 'Components/Funnels/FunnelDetails';
import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails';
import { fetchList as fetchIntegrationVariables } from 'Duck/customField'; import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
import { fetchList as fetchSiteList } from 'Duck/site'; import { fetchList as fetchSiteList } from 'Duck/site';
import { fetchList as fetchAnnouncements } from 'Duck/announcements'; import { fetchList as fetchAnnouncements } from 'Duck/announcements';
import { fetchList as fetchAlerts } from 'Duck/alerts'; import { fetchList as fetchAlerts } from 'Duck/alerts';
import { fetchWatchdogStatus } from 'Duck/watchdogs'; import { fetchWatchdogStatus } from 'Duck/watchdogs';
import { dashboardService } from "App/services"; import { dashboardService } from "App/services";
import { withStore } from 'App/mstore'
import APIClient from './api_client'; import APIClient from './api_client';
import * as routes from './routes'; import * as routes from './routes';
@ -49,9 +51,12 @@ const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails);
const withSiteId = routes.withSiteId; const withSiteId = routes.withSiteId;
const withObTab = routes.withObTab; const withObTab = routes.withObTab;
const METRICS_PATH = routes.metrics();
const METRICS_DETAILS = routes.metricDetails();
const DASHBOARD_PATH = routes.dashboard(); const DASHBOARD_PATH = routes.dashboard();
const DASHBOARD_SELECT_PATH = routes.dashboardSelected(); const DASHBOARD_SELECT_PATH = routes.dashboardSelected();
const DASHBOARD_METRICS_PATH = routes.dashboardMetricCreate(); const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate();
// const WIDGET_PATAH = routes.dashboardMetric(); // const WIDGET_PATAH = routes.dashboardMetric();
const SESSIONS_PATH = routes.sessions(); const SESSIONS_PATH = routes.sessions();
@ -69,6 +74,7 @@ const CLIENT_PATH = routes.client();
const ONBOARDING_PATH = routes.onboarding(); const ONBOARDING_PATH = routes.onboarding();
const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB); const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
@withStore
@withRouter @withRouter
@connect((state) => { @connect((state) => {
const siteId = state.getIn([ 'user', 'siteId' ]); const siteId = state.getIn([ 'user', 'siteId' ]);
@ -115,7 +121,8 @@ class Router extends React.Component {
fetchInitialData = () => { fetchInitialData = () => {
Promise.all([ Promise.all([
this.props.fetchUserInfo().then(() => { this.props.fetchUserInfo().then(() => {
dashboardService.initClient(); const { mstore } = this.props
mstore.initClient();
this.props.fetchIntegrationVariables() this.props.fetchIntegrationVariables()
}), }),
this.props.fetchSiteList().then(() => { this.props.fetchSiteList().then(() => {
@ -161,65 +168,74 @@ class Router extends React.Component {
{!hideHeader && <Header key="header"/>} {!hideHeader && <Header key="header"/>}
<Notification /> <Notification />
<Switch key="content" > <Suspense fallback={<Loader loading={true} className="flex-1" />}>
<Route path={ CLIENT_PATH } component={ Client } /> <Switch key="content" >
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } /> <Route path={ CLIENT_PATH } component={ Client } />
<Route <Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
path="/integrations/" <Route
render={ path="/integrations/"
({ location }) => { render={
const client = new APIClient(jwt); ({ location }) => {
switch (location.pathname) { const client = new APIClient(jwt);
case '/integrations/slack': switch (location.pathname) {
client.post('integrations/slack/add', { case '/integrations/slack':
code: location.search.split('=')[ 1 ], client.post('integrations/slack/add', {
state: tenantId, code: location.search.split('=')[ 1 ],
}); state: tenantId,
break; });
break;
}
return <Redirect to={ CLIENT_PATH } />;
} }
return <Redirect to={ CLIENT_PATH } />;
} }
} />
/> { onboarding &&
{ onboarding && <Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} /> }
} { siteIdList.length === 0 &&
{ siteIdList.length === 0 && <Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } /> }
}
<Route exact strict path={ withSiteId(METRICS_PATH, siteIdList) } component={ Dashboard } />
<Route exact index path={ withSiteId(DASHBOARD_SELECT_PATH, siteIdList) } component={ Dashboard } /> <Route exact strict path={ withSiteId(METRICS_DETAILS, siteIdList) } component={ Dashboard } />
<Route exact index path={ withSiteId(DASHBOARD_METRICS_PATH, siteIdList) } component={ Dashboard } />
<Route index 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(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } /> <Route exact strict path={ withSiteId(DASHBOARD_SELECT_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } /> <Route exact strict path={ withSiteId(DASHBOARD_METRIC_CREATE_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />
<Route exact strict path={ withSiteId(FUNNEL_ISSUE_PATH, siteIdList) } component={ FunnelIssue } />
<Route exact strict path={ withSiteId(SESSIONS_PATH, siteIdList) } component={ BugFinder } />
<Route exact strict path={ withSiteId(SESSION_PATH, siteIdList) } component={ Session } /> {/* <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } component={ LiveSession } /> <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } render={ (props) => <Session { ...props } live /> } /> <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
{ routes.redirects.map(([ fr, to ]) => ( <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Redirect key={ fr } exact strict from={ fr } to={ to } /> <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } /> */}
)) }
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } /> <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 } />
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />
<Route exact strict path={ withSiteId(FUNNEL_ISSUE_PATH, siteIdList) } component={ FunnelIssue } />
<Route exact strict path={ withSiteId(SESSIONS_PATH, siteIdList) } component={ BugFinder } />
<Route exact strict path={ withSiteId(SESSION_PATH, siteIdList) } component={ Session } />
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } component={ LiveSession } />
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } render={ (props) => <Session { ...props } live /> } />
{ routes.redirects.map(([ fr, to ]) => (
<Redirect key={ fr } exact strict from={ fr } to={ to } />
)) }
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } />
</Switch>
</Suspense>
</Loader>
:
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
<Switch>
<Route exact strict path={ FORGOT_PASSWORD } component={ ForgotPassword } />
<Route exact strict path={ LOGIN_PATH } component={ changePassword ? UpdatePassword : Login } />
{ !existingTenant && <Route exact strict path={ SIGNUP_PATH } component={ Signup } /> }
<Redirect to={ LOGIN_PATH } />
</Switch> </Switch>
</Loader> : </Suspense>;
<Switch>
<Route exact strict path={ FORGOT_PASSWORD } component={ ForgotPassword } />
<Route exact strict path={ LOGIN_PATH } component={ changePassword ? UpdatePassword : Login } />
{ !existingTenant && <Route exact strict path={ SIGNUP_PATH } component={ Signup } /> }
<Redirect to={ LOGIN_PATH } />
</Switch>;
} }
} }

View file

@ -1,84 +1,46 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Switch, Route, Redirect } from 'react-router';
import withPageTitle from 'HOCs/withPageTitle'; import withPageTitle from 'HOCs/withPageTitle';
import { observer } from "mobx-react-lite"; import { observer, useObserver } from "mobx-react-lite";
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import DashboardView from './components/DashboardView';
import { import {
dashboardSelected, dashboardSelected,
dashboardMetricDetails,
dashboardMetricCreate,
withSiteId, withSiteId,
dashboardMetrics,
} from 'App/routes'; } from 'App/routes';
import DashboardSideMenu from './components/DashboardSideMenu'; import DashboardSideMenu from './components/DashboardSideMenu';
import WidgetView from './components/WidgetView'; import { Loader } from 'UI';
import MetricsView from './components/MetricsView'; import DashboardRouter from './components/DashboardRouter';
function NewDashboard(props) { function NewDashboard(props) {
const { history, match: { params: { siteId, dashboardId, metricId } } } = props; const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const { dashboardStore } = useStore(); const { dashboardStore } = useStore();
const dashboard: any = dashboardStore.selectedDashboard; const loading = useObserver(() => dashboardStore.isLoading);
useEffect(() => { useEffect(() => {
dashboardStore.fetchList(); dashboardStore.fetchList().then((resp) => {
dashboardStore.setSiteId(siteId);
}, []);
useEffect(() => {
if (dashboardId) {
dashboardStore.selectDashboardById(dashboardId);
}
if (!dashboardId) {
if (dashboardId) { if (dashboardId) {
dashboardStore.selectDashboardById(dashboardId); dashboardStore.selectDashboardById(dashboardId);
} else { } else {
dashboardStore.selectDefaultDashboard().then((resp: any) => { dashboardStore.selectDefaultDashboard().then((b) => {
history.push(withSiteId(dashboardSelected(resp.dashboardId), siteId)); if (!history.location.pathname.includes('/metrics')) {
history.push(withSiteId(dashboardSelected(b.dashboardId), siteId));
}
}); });
} }
} });
}, []); }, []);
return ( return (
<Switch> <Loader loading={loading}>
<Route exact strict path={withSiteId(dashboardMetrics(), siteId)}> <div className="page-margin container-90">
<div className="page-margin container-90"> <div className="side-menu">
<div className="side-menu"> <DashboardSideMenu siteId={siteId} />
<DashboardSideMenu />
</div>
<div className="side-menu-margined">
<MetricsView />
</div>
</div> </div>
</Route> <div className="side-menu-margined">
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}> <DashboardRouter siteId={siteId} />
<WidgetView /> </div>
</Route> </div>
{ dashboardId && ( </Loader>
<>
<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>
); );
} }

View file

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import Modal from 'react-modal';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI'; import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
@ -12,19 +11,16 @@ function DashbaordListModal(props) {
<div className="color-gray-medium uppercase p-4 text-lg">Dashboards</div> <div className="color-gray-medium uppercase p-4 text-lg">Dashboards</div>
<div> <div>
{dashboards.map((item: any) => ( {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"> <div key={ item.dashboardId } className="px-4">
<SideMenuitem <SideMenuitem
key={ item.dashboardId } key={ item.dashboardId }
active={item.dashboardId === activeDashboardId} active={item.dashboardId === activeDashboardId}
title={ item.name } title={ item.name }
iconName={ item.icon } iconName={ item.icon }
// onClick={() => onItemClick(item)} // onClick={() => onItemClick(item)} // TODO add click handler
leading = {( leading = {(
<div className="ml-2 flex items-center"> <div className="ml-2 flex items-center">
<div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div> {item.isPublic && <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>} {item.isPinned && <div className="p-1"><Icon name="pin-fill" size="16" /></div>}
</div> </div>
)} )}

View file

@ -1,9 +1,7 @@
import React from 'react'; import React from 'react';
import WidgetWrapper from '../../WidgetWrapper'; import WidgetWrapper from '../WidgetWrapper';
import { useDashboardStore } from '../../store/store';
import { useObserver } from 'mobx-react-lite'; import { useObserver } from 'mobx-react-lite';
import cn from 'classnames'; import cn from 'classnames';
import { Button } from 'UI';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, unSelectCategory }) { function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, unSelectCategory }) {
@ -30,7 +28,6 @@ function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds,
function DashboardMetricSelection(props) { function DashboardMetricSelection(props) {
const { dashboardStore } = useStore(); const { dashboardStore } = useStore();
const widgetCategories = dashboardStore?.widgetCategories; const widgetCategories = dashboardStore?.widgetCategories;
const widgetTemplates = dashboardStore?.widgetTemplates;
const [activeCategory, setActiveCategory] = React.useState<any>(widgetCategories[0]); const [activeCategory, setActiveCategory] = React.useState<any>(widgetCategories[0]);
const [selectedWidgets, setSelectedWidgets] = React.useState<any>([]); const [selectedWidgets, setSelectedWidgets] = React.useState<any>([]);
const selectedWidgetIds = selectedWidgets.map((widget: any) => widget.widgetId); const selectedWidgetIds = selectedWidgets.map((widget: any) => widget.widgetId);

View file

@ -0,0 +1,45 @@
import React from 'react';
import { Switch, Route } from 'react-router';
import { withRouter } from 'react-router-dom';
import {
metrics,
metricDetails,
dashboardSelected,
dashboardMetricCreate,
withSiteId,
} from 'App/routes';
import DashboardView from '../DashboardView';
import MetricsView from '../MetricsView';
import WidgetView from '../WidgetView';
interface Props {
history: any
match: any
}
function DashboardRouter(props: Props) {
const { match: { params: { siteId, dashboardId, metricId } } } = props;
return (
<div>
<Switch>
<Route exact strict path={withSiteId(metrics(), siteId)}>
<MetricsView />
</Route>
<Route exact strict path={withSiteId(metricDetails(), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboardId), siteId)}>
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<DashboardView siteId={siteId} />
</Route>
</Switch>
</div>
);
}
export default withRouter(DashboardRouter);

View file

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

View file

@ -3,23 +3,23 @@ import React from 'react';
import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI'; import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { withSiteId, dashboardSelected, dashboardMetrics } from 'App/routes'; import { withSiteId, dashboardSelected, metrics } from 'App/routes';
import { useModal } from 'App/components/Modal'; import { useModal } from 'App/components/Modal';
import DashbaordListModal from '../DashbaordListModal'; import DashbaordListModal from '../DashbaordListModal';
import DashboardModal from '../DashboardModal'; import DashboardModal from '../DashboardModal';
const SHOW_COUNT = 5; const SHOW_COUNT = 5;
function DashboardSideMenu(props) { interface Props {
siteId: string
history: any
}
function DashboardSideMenu(props: Props) {
const { history, siteId } = props;
const { hideModal, showModal } = useModal(); const { hideModal, showModal } = useModal();
const { history } = props;
const { dashboardStore } = useStore(); const { dashboardStore } = useStore();
const dashboardId = dashboardStore.selectedDashboard?.dashboardId; const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
const dashboardsPicked = dashboardStore.dashboards.slice(0, SHOW_COUNT); const dashboardsPicked = dashboardStore.dashboards.slice(0, SHOW_COUNT);
const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT; const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT;
// React.useEffect(() => {
// showModal(<DashbaordListModal />, {});
// }, []);
const redirect = (path) => { const redirect = (path) => {
history.push(path); history.push(path);
@ -27,7 +27,7 @@ function DashboardSideMenu(props) {
const onItemClick = (dashboard) => { const onItemClick = (dashboard) => {
dashboardStore.selectDashboardById(dashboard.dashboardId); dashboardStore.selectDashboardById(dashboard.dashboardId);
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(dashboardStore.siteId)); const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(siteId));
history.push(path); history.push(path);
}; };
@ -36,7 +36,7 @@ function DashboardSideMenu(props) {
showModal(<DashboardModal />, {}) showModal(<DashboardModal />, {})
} }
return ( return useObserver(() => (
<div> <div>
<SideMenuHeader className="mb-4" text="Dashboards" /> <SideMenuHeader className="mb-4" text="Dashboards" />
{dashboardsPicked.map((item: any) => ( {dashboardsPicked.map((item: any) => (
@ -48,7 +48,7 @@ function DashboardSideMenu(props) {
onClick={() => onItemClick(item)} onClick={() => onItemClick(item)}
leading = {( leading = {(
<div className="ml-2 flex items-center"> <div className="ml-2 flex items-center">
<div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div> {item.isPublic && <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>} {item.isPinned && <div className="p-1"><Icon name="pin-fill" size="16" /></div>}
</div> </div>
)} )}
@ -79,7 +79,7 @@ function DashboardSideMenu(props) {
id="menu-manage-alerts" id="menu-manage-alerts"
title="Metrics" title="Metrics"
iconName="bar-chart-line" iconName="bar-chart-line"
onClick={() => redirect(withSiteId(dashboardMetrics(), dashboardStore.siteId))} onClick={() => redirect(withSiteId(metrics(), siteId))}
/> />
</div> </div>
<div className="border-t w-full my-2" /> <div className="border-t w-full my-2" />
@ -92,7 +92,7 @@ function DashboardSideMenu(props) {
/> />
</div> </div>
</div> </div>
); ));
} }
export default withRouter(observer(DashboardSideMenu)); export default withRouter(DashboardSideMenu);

View file

@ -7,9 +7,10 @@ import withModal from 'App/components/Modal/withModal';
import DashboardWidgetGrid from '../DashboardWidgetGrid'; import DashboardWidgetGrid from '../DashboardWidgetGrid';
interface Props { interface Props {
siteId: number;
} }
function DashboardView(props: Props) { function DashboardView(props: Props) {
const { siteId } = props;
const { dashboardStore } = useStore(); const { dashboardStore } = useStore();
const dashboard: any = dashboardStore.selectedDashboard const dashboard: any = dashboardStore.selectedDashboard
@ -18,7 +19,7 @@ function DashboardView(props: Props) {
<div className="flex items-center mb-4 justify-between"> <div className="flex items-center mb-4 justify-between">
<div className="flex items-center"> <div className="flex items-center">
<PageTitle title={dashboard.name} className="mr-3" /> <PageTitle title={dashboard.name} className="mr-3" />
<Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), dashboardStore.siteId)}><Button primary size="small">Add Metric</Button></Link> <Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}><Button primary size="small">Add Metric</Button></Link>
</div> </div>
<div> <div>
Right Right

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import WidgetWrapper from '../../WidgetWrapper'; import WidgetWrapper from '../WidgetWrapper';
import { NoContent, Button, Loader } from 'UI'; import { NoContent, Button, Loader } from 'UI';
import { useObserver } from 'mobx-react-lite'; import { useObserver } from 'mobx-react-lite';

View file

@ -0,0 +1,48 @@
import React from 'react';
import { Icon, NoContent, Label, Link, Pagination } from 'UI';
interface Props {
metric: any;
}
function DashboardLink({ dashboards}) {
return (
dashboards.map(dashboard => (
<Link to={`/dashboard/${dashboard.dashboardId}`} className="">
<div className="flex items-center">
<div className="mr-2 text-4xl no-underline" style={{ textDecoration: 'none'}}>·</div>
<span className="link">{dashboard.name}</span>
</div>
</Link>
))
);
}
function MetricListItem(props: Props) {
const { metric } = props;
return (
<div className="grid grid-cols-7 p-3 border-t select-none">
<div className="col-span-2">
<Link to={`/metrics/${metric.metricId}`} className="link">
{metric.name}
</Link>
</div>
<div><Label className="capitalize">{metric.metricType}</Label></div>
<div>
<DashboardLink dashboards={metric.dashboards} />
{/* <div className="link flex items-center">
<div className="mr-2 text-4xl">·</div>
<span>Dashboards</span>
</div> */}
</div>
<div>{metric.owner}</div>
{/* <div className="flex items-center">
<Icon name={metric.isPublic ? "user-friends" : "person-fill"} className="mr-2" />
<span>{metric.isPublic ? 'Team' : 'Private'}</span>
</div> */}
<div>Last Modified</div>
</div>
);
}
export default MetricListItem;

View file

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

View file

@ -1,24 +1,20 @@
import { useObserver } from 'mobx-react-lite'; import { useObserver } from 'mobx-react-lite';
import React from 'react'; import React from 'react';
import { Icon, NoContent, Label, Link, Pagination } from 'UI'; import { NoContent, Pagination } from 'UI';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { getRE } from 'App/utils'; import { getRE } from 'App/utils';
import MetricListItem from '../MetricListItem';
interface Props { } interface Props { }
function MetricsList(props: Props) { function MetricsList(props: Props) {
const { dashboardStore } = useStore(); const { metricStore } = useStore();
const widgets = dashboardStore.widgets; const metrics = useObserver(() => metricStore.metrics);
const lenth = widgets.length; const lenth = metrics.length;
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 metricsSearch = useObserver(() => metricStore.metricsSearch);
const pageSize = dashboardStore.metricsPageSize; const filterRE = getRE(metricsSearch, 'i');
const start = (currentPage - 1) * pageSize; const list = metrics.filter(w => filterRE.test(w.name));
const end = currentPage * pageSize;
return useObserver(() => ( return useObserver(() => (
<NoContent show={lenth === 0} icon="exclamation-circle"> <NoContent show={lenth === 0} icon="exclamation-circle">
@ -28,44 +24,21 @@ function MetricsList(props: Props) {
<div>Type</div> <div>Type</div>
<div>Dashboards</div> <div>Dashboards</div>
<div>Owner</div> <div>Owner</div>
<div>Visibility & Edit Access</div> {/* <div>Visibility & Edit Access</div> */}
<div>Last Modified</div> <div>Last Modified</div>
</div> </div>
{list.slice(start, end).map((metric: any) => ( {list.map((metric: any) => (
<div className="grid grid-cols-7 p-3 border-t select-none"> <MetricListItem metric={metric} />
<div className="col-span-2">
<Link to="/dashboard/metrics/create" className="link">
{metric.name}
</Link>
</div>
<div><Label className="capitalize">{metric.metricType}</Label></div>
<div>Dashboards</div>
<div>{metric.owner}</div>
<div>
{metric.isPrivate ? (
<div className="flex items-center">
<Icon name="person-fill" className="mr-2" />
<span>Private</span>
</div>
) : (
<div className="flex items-center">
<Icon name="user-friends" className="mr-2" />
<span>Team</span>
</div>
)}
</div>
<div>Last Modified</div>
</div>
))} ))}
</div> </div>
<div className="w-full flex items-center justify-center py-6"> <div className="w-full flex items-center justify-center py-6">
<Pagination <Pagination
page={currentPage} page={metricStore.page}
totalPages={Math.ceil(totalPages / pageSize)} totalPages={Math.ceil(lenth / metricStore.pageSize)}
onPageChange={(page) => dashboardStore.updateKey('metricsPage', page)} onPageChange={(page) => metricStore.updateKey('page', page)}
limit={pageSize} limit={metricStore.pageSize}
debounceRequest={100} debounceRequest={100}
/> />
</div> </div>

View file

@ -3,9 +3,16 @@ import { Button, PageTitle, Icon, Link } from 'UI';
import { withSiteId, dashboardMetricCreate } from 'App/routes'; import { withSiteId, dashboardMetricCreate } from 'App/routes';
import MetricsList from '../MetricsList'; import MetricsList from '../MetricsList';
import MetricsSearch from '../MetricsSearch'; import MetricsSearch from '../MetricsSearch';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
function MetricsView(props) { function MetricsView(props) {
return ( const { metricStore } = useStore();
React.useEffect(() => {
metricStore.fetchList();
}, []);
return useObserver(() => (
<div> <div>
<div className="flex items-center mb-4 justify-between"> <div className="flex items-center mb-4 justify-between">
<PageTitle title="Metrics" className="mr-3" /> <PageTitle title="Metrics" className="mr-3" />
@ -16,7 +23,7 @@ function MetricsView(props) {
</div> </div>
<MetricsList /> <MetricsList />
</div> </div>
); ));
} }
export default MetricsView; export default MetricsView;

View file

@ -0,0 +1,27 @@
import React from 'react';
interface Props {
metric: any;
}
function WidgetChart(props: Props) {
const { metric } = props;
const renderChart = () => {
const { metricType } = metric;
if (metricType === 'timeseries') {
return <div>Chart</div>;
}
if (metricType === 'table') {
return <div>Table</div>;
}
return <div>Unknown</div>;
}
return (
<div>
{renderChart()}
</div>
);
}
export default WidgetChart;

View file

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

View file

@ -7,16 +7,19 @@ import { useObserver } from 'mobx-react-lite';
import { HelpText, Button, Icon } from 'UI' import { HelpText, Button, Icon } from 'UI'
import FilterSeries from '../FilterSeries'; import FilterSeries from '../FilterSeries';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { confirm } from 'UI/Confirmation';
interface Props { interface Props {
history: any; history: any;
match: any; match: any;
onDelete: () => void;
} }
function WidgetForm(props: Props) { function WidgetForm(props: Props) {
const { history, match: { params: { siteId, dashboardId, metricId } } } = props; const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
const { dashboardStore } = useStore(); console.log('WidgetForm params', props.match.params);
const metric: any = dashboardStore.currentWidget; const { metricStore } = useStore();
const metric: any = metricStore.instance;
const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries'); const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries');
const tableOptions = metricOf.filter(i => i.type === 'table'); const tableOptions = metricOf.filter(i => i.type === 'table');
@ -24,31 +27,44 @@ function WidgetForm(props: Props) {
const isTimeSeries = metric.metricType === 'timeseries'; const isTimeSeries = metric.metricType === 'timeseries';
const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions); const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions);
const write = ({ target: { value, name } }) => dashboardStore.editWidget({ [ name ]: value }); const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value });
const writeOption = (e, { value, name }) => { const writeOption = (e, { value, name }) => {
dashboardStore.editWidget({ [ name ]: value }); metricStore.merge({ [ name ]: value });
if (name === 'metricValue') { if (name === 'metricValue') {
dashboardStore.editWidget({ metricValue: [value] }); metricStore.merge({ metricValue: [value] });
} }
if (name === 'metricOf') { if (name === 'metricOf') {
if (value === FilterKey.ISSUE) { if (value === FilterKey.ISSUE) {
dashboardStore.editWidget({ metricValue: ['all'] }); metricStore.merge({ metricValue: ['all'] });
} }
} }
if (name === 'metricType') { if (name === 'metricType') {
if (value === 'timeseries') { if (value === 'timeseries') {
dashboardStore.editWidget({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }); metricStore.merge({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' });
} else if (value === 'table') { } else if (value === 'table') {
dashboardStore.editWidget({ metricOf: tableOptions[0].value, viewType: 'table' }); metricStore.merge({ metricOf: tableOptions[0].value, viewType: 'table' });
} }
} }
}; };
const onSave = () => { const onSave = () => {
dashboardStore.saveMetric(metric, dashboardId); metricStore.save(metric, dashboardId);
}
const onDelete = async () => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, Delete',
confirmation: `Are you sure you want to permanently delete this metric?`
})) {
metricStore.delete(metric).then(props.onDelete);
// props.remove(instance.alertId).then(() => {
// toggleForm(null, false);
// });
}
} }
return useObserver(() => ( return useObserver(() => (
@ -88,15 +104,15 @@ function WidgetForm(props: Props) {
)} )}
{metric.metricOf === FilterKey.ISSUE && ( {metric.metricOf === FilterKey.ISSUE && (
<> <>
<span className="mx-3">issue type</span> <span className="mx-3">issue type</span>
<DropdownPlain <DropdownPlain
name="metricValue" name="metricValue"
options={_issueOptions} options={_issueOptions}
value={ metric.metricValue[0] } value={ metric.metricValue[0] }
onChange={ writeOption } onChange={ writeOption }
/> />
</> </>
)} )}
{metric.metricType === 'table' && ( {metric.metricType === 'table' && (
@ -154,9 +170,9 @@ function WidgetForm(props: Props) {
Save Save
</Button> </Button>
<div className="flex items-center"> <div className="flex items-center">
{metric.widgetId && ( {metric.exists() && (
<> <>
<Button plain size="small" className="flex items-center"> <Button plain size="small" onClick={onDelete} className="flex items-center">
<Icon name="trash" size="14" className="mr-2" color="teal"/> <Icon name="trash" size="14" className="mr-2" color="teal"/>
Delete Delete
</Button> </Button>
@ -172,4 +188,4 @@ function WidgetForm(props: Props) {
)); ));
} }
export default withRouter(WidgetForm); export default WidgetForm;

View file

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

View file

@ -4,38 +4,65 @@ import { useStore } from 'App/mstore';
import WidgetForm from '../WidgetForm'; import WidgetForm from '../WidgetForm';
import WidgetPreview from '../WidgetPreview'; import WidgetPreview from '../WidgetPreview';
import WidgetSessions from '../WidgetSessions'; import WidgetSessions from '../WidgetSessions';
import { Icon } from 'UI'; import { Icon, BackLink, Loader } from 'UI';
import { useObserver } from 'mobx-react-lite';
import { withSiteId } from 'App/routes';
interface Props { interface Props {
history: any;
match: any
siteId: any
} }
function WidgetView(props: Props) { function WidgetView(props: Props) {
const { match: { params: { siteId, dashboardId, metricId } } } = props;
const [expanded, setExpanded] = useState(true); const [expanded, setExpanded] = useState(true);
const { dashboardStore } = useStore(); const { metricStore } = useStore();
const widget = dashboardStore.currentWidget; const widget = useObserver(() => metricStore.instance);
return ( const loading = useObserver(() => metricStore.isLoading);
<div className="page-margin container-70 mb-8">
<div className="bg-white rounded border"> React.useEffect(() => {
<div className="p-4 flex justify-between items-center"> if (metricId && metricId !== 'create') {
<h1 className="mb-0 text-2xl">{widget.name}</h1> metricStore.fetch(metricId).then((metric) => {
<div className="text-gray-600"> // metricStore.init(metric)
<div });
onClick={() => setExpanded(!expanded)} } else {
className="flex items-center cursor-pointer select-none" metricStore.init();
> }
<span className="mr-2 color-teal">{expanded ? 'Collapse' : 'Expand'}</span> }, [])
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} size="16" color="teal" />
const onBackHandler = () => {
if (dashboardId) {
props.history.push(withSiteId(`/dashboard/${dashboardId}`, siteId));
} {
props.history.push(withSiteId(`/metrics`, siteId));
}
}
return useObserver(() => (
<Loader loading={loading}>
<div className="relative">
<BackLink onClick={onBackHandler} vertical className="absolute" style={{ left: '-50px', top: '0px' }} />
<div className="bg-white rounded border">
<div className="p-4 flex justify-between items-center">
<h1 className="mb-0 text-2xl">{widget.name}</h1>
<div className="text-gray-600">
<div
onClick={() => setExpanded(!expanded)}
className="flex items-center cursor-pointer select-none"
>
<span className="mr-2 color-teal">{expanded ? 'Collapse' : 'Expand'}</span>
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} size="16" color="teal" />
</div>
</div> </div>
</div> </div>
{ expanded && <WidgetForm onDelete={onBackHandler} {...props}/>}
</div> </div>
{ expanded && <WidgetForm />} <WidgetPreview className="mt-8" />
<WidgetSessions className="mt-8" />
</div> </div>
</Loader>
<WidgetPreview className="mt-8" /> ));
<WidgetSessions className="mt-8" />
</div>
);
} }
export default withRouter(WidgetView); export default WidgetView;

View file

@ -1,8 +1,8 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { useDashboardStore } from '../store/store';
import cn from 'classnames'; import cn from 'classnames';
import { ItemMenu } from 'UI'; import { ItemMenu } from 'UI';
import { useDrag, useDrop } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd';
import WidgetChart from '../WidgetChart';
interface Props { interface Props {
className?: string; className?: string;
@ -46,7 +46,7 @@ function WidgetWrapper(props: Props) {
style={{ style={{
userSelect: 'none', userSelect: 'none',
opacity: isDragging ? 0.5 : 1, opacity: isDragging ? 0.5 : 1,
borderColor: canDrop && isOver ? '#394EFF' : '#EEE', borderColor: canDrop && isOver ? '#394EFF' : '',
}} }}
ref={dragDropRef} ref={dragDropRef}
> >
@ -54,7 +54,7 @@ function WidgetWrapper(props: Props) {
<div <div
className="p-3 cursor-move border-b flex items-center justify-between" className="p-3 cursor-move border-b flex items-center justify-between"
> >
{widget.name} - {widget.position} {widget.name}
<div> <div>
<ItemMenu <ItemMenu
items={[ items={[
@ -66,14 +66,11 @@ function WidgetWrapper(props: Props) {
}, },
]} ]}
/> />
{/* <button className="btn btn-sm btn-outline-primary" onClick={() => dashboard.removeWidget(widget.widgetId)}>
remove
</button> */}
</div> </div>
</div> </div>
<div className="h-40"> <div className="h-40">
<WidgetChart metric={props.widget} />
</div> </div>
{/* </Link> */} {/* </Link> */}
</div> </div>

View file

@ -82,13 +82,13 @@ export default class Dashboard implements IDashboard {
this.dashboardId = json.dashboardId this.dashboardId = json.dashboardId
this.name = json.name this.name = json.name
this.isPublic = json.isPublic this.isPublic = json.isPublic
this.isPinned = json.isPinned
this.widgets = json.widgets ? json.widgets.map(w => new Widget().fromJson(w)) : [] this.widgets = json.widgets ? json.widgets.map(w => new Widget().fromJson(w)) : []
}) })
return this return this
} }
validate() { validate() {
console.log('called...')
return this.isValid = this.name.length > 0 return this.isValid = this.name.length > 0
} }

View file

@ -167,12 +167,12 @@ export default class DashboardStore implements IDashboardSotre {
save(dashboard: IDashboard): Promise<any> { save(dashboard: IDashboard): Promise<any> {
this.isSaving = true this.isSaving = true
const isCreating = !dashboard.dashboardId const isCreating = !dashboard.dashboardId
return dashboardService.saveDashboard(dashboard).then(response => { return dashboardService.saveDashboard(dashboard).then(_dashboard => {
runInAction(() => { runInAction(() => {
if (isCreating) { if (isCreating) {
this.addDashboard(response.data) this.addDashboard(_dashboard)
} else { } else {
this.updateDashboard(response.data) this.updateDashboard(_dashboard)
} }
}) })
}).finally(() => { }).finally(() => {

View file

@ -1 +1 @@
export { default } from './dashboardStore' export { default as DashboardStore } from './dashboardStore';

View file

@ -32,6 +32,7 @@ export interface IWidget {
update(data: any): void update(data: any): void
} }
export default class Widget implements IWidget { export default class Widget implements IWidget {
public static get ID_KEY():string { return "widgetId" }
widgetId: any = undefined widgetId: any = undefined
name: string = "New Metric" name: string = "New Metric"
metricType: string = "timeseries" metricType: string = "timeseries"

View file

@ -6,7 +6,7 @@ export default function BackLink ({
className, to, onClick, label, vertical = false, style className, to, onClick, label, vertical = false, style
}) { }) {
const children = ( const children = (
<div className={ cn('flex items-center', {'border w-10 h-10 rounded-full bg-white p-3 items-center justify-center' : !label })}> <div className={ cn('flex items-center', {'border w-10 h-10 rounded-full bg-white p-3 items-center justify-center hover:bg-active-blue' : !label })}>
<Icon color="gray-dark" className={ cls.icon } name="prev1" size="16" /> <Icon color="gray-dark" className={ cls.icon } name="prev1" size="16" />
{ label && <div className="ml-1">{ label }</div> } { label && <div className="ml-1">{ label }</div> }
</div> </div>

View file

@ -10,9 +10,6 @@ import { ModalProvider } from './components/Modal';
import ModalRoot from './components/Modal/ModalRoot'; import ModalRoot from './components/Modal/ModalRoot';
import { HTML5Backend } from 'react-dnd-html5-backend' import { HTML5Backend } from 'react-dnd-html5-backend'
import { DndProvider } from 'react-dnd' import { DndProvider } from 'react-dnd'
import Modal from 'react-modal';
Modal.setAppElement('#modal-root');
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
render( render(

View file

@ -0,0 +1,341 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import Dashboard, { IDashboard } from "./types/dashboard"
import Widget, { IWidget } from "./types/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(): Promise<any>
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()
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 = true
isSaving: boolean = false
constructor() {
makeAutoObservable(this, {
resetCurrentWidget: action,
addDashboard: action,
removeDashboard: action,
updateDashboard: action,
getDashboard: action,
getDashboardIndex: action,
getDashboardByIndex: action,
getDashboardCount: action,
getDashboardIndexByDashboardId: action,
selectDashboardById: action,
selectDefaultDashboard: action,
toJson: action,
fromJson: action,
setSiteId: action,
editWidget: action,
updateKey: action,
})
// TODO remove this sample data
// this.dashboards = sampleDashboards
// 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 = {
name: `Category ${i + 1}`,
categoryId: i,
description: `Category ${i + 1} description`,
widgets: []
}
const randomNumber = Math.floor(Math.random() * (5 - 2 + 1)) + 2
for (let j = 0; j < randomNumber; j++) {
const widget: any= {};
widget.widgetId = `${i}-${j}`
widget.name = `Widget ${i}-${j}`;
widget.metricType = ['timeseries', 'table'][Math.floor(Math.random() * 2)];
cat.widgets.push(widget);
}
this.widgetCategories.push(cat)
}
}
findByIds(ids: string[]) {
return this.dashboards.filter(d => ids.includes(d.dashboardId))
}
initDashboard(dashboard: Dashboard) {
this.dashboardInstance = dashboard || new Dashboard()
}
updateKey(key: any, value: any) {
this[key] = value
}
resetCurrentWidget() {
this.currentWidget = new Widget()
}
editWidget(widget: any) {
this.currentWidget.update(widget)
}
fetchList(): Promise<any> {
this.isLoading = true
return dashboardService.getDashboards()
.then((list: any) => {
runInAction(() => {
this.dashboards = list.map(d => new Dashboard().fromJson(d))
})
}).finally(() => {
runInAction(() => {
this.isLoading = false
})
})
}
fetch(dashboardId: string) {
this.isLoading = true
dashboardService.getDashboard(dashboardId).then(response => {
runInAction(() => {
this.selectedDashboard = new Dashboard().fromJson(response)
})
}).finally(() => {
runInAction(() => {
this.isLoading = false
})
})
}
save(dashboard: IDashboard): Promise<any> {
this.isSaving = true
const isCreating = !dashboard.dashboardId
return dashboardService.saveDashboard(dashboard).then(_dashboard => {
runInAction(() => {
if (isCreating) {
this.addDashboard(_dashboard)
} else {
this.updateDashboard(_dashboard)
}
})
}).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) {
widget.validate()
if (widget.isValid) {
this.isLoading = true
}
}
delete(dashboard: Dashboard) {
this.isLoading = true
}
toJson() {
return {
dashboards: this.dashboards.map(d => d.toJson())
}
}
fromJson(json: any) {
runInAction(() => {
this.dashboards = json.dashboards.map(d => new Dashboard().fromJson(d))
})
return this
}
addDashboard(dashboard: Dashboard) {
this.dashboards.push(dashboard)
}
removeDashboard(dashboard: Dashboard) {
this.dashboards = this.dashboards.filter(d => d !== dashboard)
}
getDashboard(dashboardId: string) {
return this.dashboards.find(d => d.dashboardId === dashboardId)
}
getDashboardIndex(dashboardId: string) {
return this.dashboards.findIndex(d => d.dashboardId === dashboardId)
}
getDashboardByIndex(index: number) {
return this.dashboards[index]
}
getDashboardCount() {
return this.dashboards.length
}
getDashboardIndexByDashboardId(dashboardId: string) {
return this.dashboards.findIndex(d => d.dashboardId === dashboardId)
}
updateDashboard(dashboard: Dashboard) {
const index = this.dashboards.findIndex(d => d.dashboardId === dashboard.dashboardId)
if (index >= 0) {
this.dashboards[index] = dashboard
}
}
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 = (): Promise<Dashboard> => {
return new Promise((resolve, reject) => {
if (this.dashboards.length > 0) {
const pinnedDashboard = this.dashboards.find(d => d.isPinned)
if (pinnedDashboard) {
console.log('selecting pined dashboard')
this.selectedDashboard = pinnedDashboard
} else {
console.log('selecting first dashboard')
this.selectedDashboard = this.dashboards[0]
}
}
if (this.selectedDashboard) {
resolve(this.selectedDashboard)
}
reject(new Error("No dashboards found"))
})
}
}
function getRandomWidget() {
const widget = new Widget();
widget.widgetId = Math.floor(Math.random() * 100);
widget.name = randomMetricName();
// widget.type = "random";
widget.colSpan = Math.floor(Math.random() * 2) + 1;
return widget;
}
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 = 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);
}
return dashboard;
}
const sampleDashboards = [
getRandomDashboard(1, true),
getRandomDashboard(2),
getRandomDashboard(3),
getRandomDashboard(4),
]

View file

@ -1,10 +1,22 @@
import React from 'react'; import React from 'react';
import DashboardStore, { IDashboardSotre } from 'App/components/Dashboard/store/DashboardStore'; import DashboardStore, { IDashboardSotre } from './dashboardStore';
import MetricStore, { IMetricStore } from './metricStore';
import APIClient from 'App/api_client';
import { dashboardService, metricService } from 'App/services';
export class RootStore { export class RootStore {
dashboardStore: IDashboardSotre; dashboardStore: IDashboardSotre;
metricStore: IMetricStore;
constructor() { constructor() {
this.dashboardStore = new DashboardStore(); this.dashboardStore = new DashboardStore();
this.metricStore = new MetricStore();
}
initClient() {
const client = new APIClient();
dashboardService.initClient(client)
metricService.initClient(client)
} }
} }
@ -19,6 +31,5 @@ export const StoreProvider = ({ children, store }) => {
export const useStore = () => React.useContext(StoreContext); export const useStore = () => React.useContext(StoreContext);
export const withStore = (Component) => (props) => { export const withStore = (Component) => (props) => {
return <Component {...props} store={useStore()} />; return <Component {...props} mstore={useStore()} />;
}; };

View file

@ -0,0 +1,177 @@
import { makeAutoObservable, runInAction, observable, action, reaction, computed } from "mobx"
import Widget, { IWidget } from "./types/widget";
import { metricService } from "App/services";
export interface IMetricStore {
paginatedList: any;
isLoading: boolean
isSaving: boolean
metrics: IWidget[]
instance: IWidget
page: number
pageSize: number
metricsSearch: string
sort: any
// State Actions
init(metric?: IWidget|null): void
updateKey(key: string, value: any): void
merge(object: any): void
reset(meitricId: string): void
addToList(metric: IWidget): void
updateInList(metric: IWidget): void
findById(metricId: string): void
removeById(metricId: string): void
// API
save(metric: IWidget, dashboardId?: string): Promise<any>
fetchList(): void
fetch(metricId: string)
delete(metric: IWidget)
}
export default class MetricStore implements IMetricStore {
isLoading: boolean = false
isSaving: boolean = false
metrics: IWidget[] = []
instance: IWidget = new Widget()
page: number = 1
pageSize: number = 10
metricsSearch: string = ""
sort: any = {}
constructor() {
makeAutoObservable(this, {
isLoading: observable,
metrics: observable,
instance: observable,
page: observable,
pageSize: observable,
metricsSearch: observable,
sort: observable,
init: action,
updateKey: action,
merge: action,
reset: action,
addToList: action,
updateInList: action,
findById: action,
removeById: action,
save: action,
fetchList: action,
fetch: action,
delete: action,
paginatedList: computed,
})
// reaction(
// () => this.metricsSearch,
// (metricsSearch) => { // TODO filter the list for View
// console.log('metricsSearch', metricsSearch)
// this.page = 1
// this.paginatedList()
// }
// )
}
// State Actions
init(metric?: IWidget|null) {
this.instance = metric || new Widget()
}
updateKey(key: string, value: any) {
this.instance[key] = value
}
merge(object: any) {
this.instance = Object.assign(this.instance, object)
}
reset(id: string) {
const metric = this.findById(id)
if (metric) {
this.instance = metric
}
}
addToList(metric: IWidget) {
this.metrics.push(metric)
}
updateInList(metric: IWidget) {
const index = this.metrics.findIndex((m: IWidget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY])
if (index >= 0) {
this.metrics[index] = metric
}
}
findById(id: string) {
return this.metrics.find(m => m[Widget.ID_KEY] === id)
}
removeById(id: string): void {
this.metrics = this.metrics.filter(m => m[Widget.ID_KEY] !== id)
}
get paginatedList(): IWidget[] {
console.log('here...' + this.page)
const start = (this.page - 1) * this.pageSize
const end = start + this.pageSize
return this.metrics.slice(start, end)
}
// API Communication
save(metric: IWidget, dashboardId?: string) {
const wasCreating = !metric[Widget.ID_KEY]
this.isSaving = true
return metricService.saveMetric(metric, dashboardId)
.then(() => {
if (wasCreating) {
this.addToList(metric)
} else {
this.updateInList(metric)
}
}).finally(() => {
this.isSaving = false
})
}
fetchList() {
this.isLoading = true
return metricService.getMetrics()
.then(metrics => {
this.metrics = metrics
}).finally(() => {
this.isLoading = false
})
}
fetch(id: string) {
this.isLoading = true
return metricService.getMetric(id)
.then(metric => {
return this.instance = new Widget().fromJson(metric)
}).finally(() => {
this.isLoading = false
})
}
delete(metric: IWidget) {
this.isSaving = true
return metricService.deleteMetric(metric[Widget.ID_KEY])
.then(() => {
this.removeById(metric[Widget.ID_KEY])
}).finally(() => {
this.isSaving = false
})
}
}

View file

@ -0,0 +1,152 @@
import { makeAutoObservable, observable, action, runInAction } from "mobx"
import Widget, { IWidget } from "./widget"
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 {
public static get ID_KEY():string { return "dashboardId" }
dashboardId: any = undefined
name: string = "New Dashboard X"
isPublic: boolean = false
widgets: IWidget[] = []
isValid: boolean = false
isPinned: boolean = false
currentWidget: IWidget = new Widget()
constructor() {
makeAutoObservable(this, {
name: observable,
isPublic: observable,
widgets: observable,
isValid: observable,
toJson: action,
fromJson: action,
addWidget: action,
removeWidget: action,
updateWidget: action,
getWidget: action,
getWidgetIndex: action,
getWidgetByIndex: action,
getWidgetCount: action,
getWidgetIndexByWidgetId: action,
validate: action,
sortWidgets: action,
swapWidgetPosition: action,
update: action,
})
this.validate();
}
update(data: any) {
runInAction(() => {
Object.assign(this, data)
})
this.validate()
}
toJson() {
return {
dashboardId: this.dashboardId,
name: this.name,
isPrivate: this.isPublic,
widgets: this.widgets.map(w => w.toJson())
}
}
fromJson(json: any) {
runInAction(() => {
this.dashboardId = json.dashboardId
this.name = json.name
this.isPublic = json.isPublic
this.isPinned = json.isPinned
this.widgets = json.widgets ? json.widgets.map(w => new Widget().fromJson(w)) : []
})
return this
}
validate() {
return this.isValid = this.name.length > 0
}
addWidget(widget: IWidget) {
this.widgets.push(widget)
}
removeWidget(widgetId: string) {
this.widgets = this.widgets.filter(w => w.widgetId !== widgetId)
}
updateWidget(widget: IWidget) {
const index = this.widgets.findIndex(w => w.widgetId === widget.widgetId)
if (index >= 0) {
this.widgets[index] = widget
}
}
getWidget(widgetId: string) {
return this.widgets.find(w => w.widgetId === widgetId)
}
getWidgetIndex(widgetId: string) {
return this.widgets.findIndex(w => w.widgetId === widgetId)
}
getWidgetByIndex(index: number) {
return this.widgets[index]
}
getWidgetCount() {
return this.widgets.length
}
getWidgetIndexByWidgetId(widgetId: string) {
return this.widgets.findIndex(w => w.widgetId === widgetId)
}
swapWidgetPosition(positionA, positionB) {
console.log('swapWidgetPosition', positionA, positionB)
const widgetA = this.widgets[positionA]
const widgetB = this.widgets[positionB]
this.widgets[positionA] = widgetB
this.widgets[positionB] = widgetA
widgetA.position = positionB
widgetB.position = positionA
}
sortWidgets() {
this.widgets = this.widgets.sort((a, b) => {
if (a.position > b.position) {
return 1
} else if (a.position < b.position) {
return -1
} else {
return 0
}
})
}
}

View file

@ -0,0 +1,61 @@
import { filter } from "App/components/BugFinder/ManageFilters/savedFilterList.css"
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import { FilterKey, FilterType } from 'Types/filter/filterType'
import { filtersMap } from 'Types/filter/newFilter'
import FilterItem from "./filterItem"
// console.log('filtersMap', filtersMap)
export default class Filter {
public static get ID_KEY():string { return "filterId" }
name: string = ''
filters: any[] = []
eventsOrder: string = 'then'
constructor() {
makeAutoObservable(this, {
addFilter: action,
removeFilter: action,
updateKey: action,
})
}
addFilter(filter: any) {
filter.value = [""]
if (filter.hasOwnProperty('filters')) {
filter.filters = filter.filters.map(i => {
i.value = [""]
return new FilterItem(i)
})
}
this.filters.push(new FilterItem(filter))
}
updateFilter(index: number, filter: any) {
this.filters[index] = new FilterItem(filter)
}
updateKey(key, value) {
this[key] = value
}
removeFilter(index: number) {
this.filters.splice(index, 1)
}
fromJson(json) {
this.name = json.name
this.filters = json.filters.map(i => new FilterItem().fromJson(i))
this.eventsOrder = json.eventsOrder
return this
}
toJson() {
const json = {
name: this.name,
filters: this.filters.map(i => i.toJson()),
eventsOrder: this.eventsOrder,
}
return json
}
}

View file

@ -0,0 +1,66 @@
import { filter } from "App/components/BugFinder/ManageFilters/savedFilterList.css"
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import { FilterKey, FilterType } from 'Types/filter/filterType'
import { filtersMap } from 'Types/filter/newFilter'
export default class FilterItem {
type: string = ''
key: string = ''
label: string = ''
value: any = [""]
isEvent: boolean = false
operator: string = ''
source: string = ''
filters: FilterItem[] = []
operatorOptions: any[] = []
constructor(data: any = {}) {
makeAutoObservable(this, {
type: observable,
key: observable,
value: observable,
})
this.merge(data)
}
merge(data) {
Object.keys(data).forEach(key => {
this[key] = data[key]
})
}
fromJson(json, mainFilterKey = '') {
let _filter = filtersMap[json.type]
if (mainFilterKey) {
const mainFilter = filtersMap[mainFilterKey];
const subFilterMap = {}
mainFilter.filters.forEach(option => {
subFilterMap[option.key] = option
})
_filter = subFilterMap[json.type]
}
this.type = _filter.type
this.key = _filter.key
this.label = _filter.label
this.operatorOptions = _filter.operatorOptions
this.value = json.value.length === 0 || !json.value ? [""] : json.value,
this.operator = json.operator
this.isEvent = _filter.isEvent
this.filters = _filter.type === FilterType.SUB_FILTERS && json.filters ? json.filters.map(i => new FilterItem().fromJson(i, json.type)) : []
return this
}
toJson() {
const json = {
type: this.key,
isEvent: this.isEvent,
value: this.value,
operator: this.operator,
source: this.source,
filters: this.filters.map(i => i.toJson()),
}
return json
}
}

View file

@ -0,0 +1,38 @@
// import Filter from 'Types/filter';
import Filter from './filter'
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
export default class FilterSeries {
public static get ID_KEY():string { return "seriesId" }
seriesId?: any = undefined
name: string = "Series 1"
filter: Filter = new Filter()
constructor() {
makeAutoObservable(this, {
name: observable,
filter: observable,
update: action,
})
}
update(key, value) {
this[key] = value
}
fromJson(json) {
this.seriesId = json.seriesId
this.name = json.name
this.filter = new Filter().fromJson(json.filter)
return this
}
toJson() {
return {
seriesId: this.seriesId,
name: this.name,
filter: this.filter.toJson(),
}
}
}

View file

@ -0,0 +1,137 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import FilterSeries from "./filterSeries";
export interface IWidget {
metricId: string
widgetId: any
name: string
metricType: string
metricOf: string
metricValue: string
viewType: string
series: FilterSeries[]
sessions: []
isPublic: boolean
owner: string
lastModified: Date
dashboards: any[]
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
exists(): boolean
}
export default class Widget implements IWidget {
public static get ID_KEY():string { return "metricId" }
metricId: any = undefined
widgetId: any = undefined
name: string = "New Metric"
metricType: string = "timeseries"
metricOf: string = "sessionCount"
metricValue: string = ""
viewType: string = "lineChart"
series: FilterSeries[] = []
sessions: [] = []
isPublic: boolean = true
owner: string = ""
lastModified: Date = new Date()
dashboards: any[] = []
dashboardIds: any[] = []
position: number = 0
data: any = {}
isLoading: boolean = false
isValid: boolean = false
dashboardId: any = undefined
colSpan: number = 2
constructor() {
makeAutoObservable(this, {
widgetId: observable,
name: observable,
metricType: observable,
metricOf: observable,
position: observable,
data: observable,
isLoading: observable,
isValid: observable,
dashboardId: observable,
addSeries: action,
colSpan: observable,
fromJson: action,
toJson: action,
validate: action,
update: action,
udpateKey: action,
})
const filterSeries = new FilterSeries()
this.series.push(filterSeries)
}
udpateKey(key: string, value: any) {
this[key] = value
}
removeSeries(index: number) {
this.series.splice(index, 1)
}
addSeries() {
const series = new FilterSeries()
series.name = "Series " + (this.series.length + 1)
this.series.push(series)
}
fromJson(json: any) {
console.log('json', json);
runInAction(() => {
this.metricId = json.metricId
this.widgetId = json.widgetId
this.name = json.name
this.data = json.data
this.series = json.series.map((series: any) => new FilterSeries().fromJson(series)),
this.dashboards = json.dashboards
})
return this
}
toJson() {
return {
metricId: this.metricId,
widgetId: this.widgetId,
metricOf: this.metricOf,
metricValue: this.metricValue,
metricType: this.metricType,
name: this.name,
series: this.series.map((series: any) => series.toJson()),
}
}
validate() {
this.isValid = this.name.length > 0
}
update(data: any) {
runInAction(() => {
Object.assign(this, data)
})
}
exists() {
return this.metricId !== undefined
}
}

View file

@ -105,9 +105,10 @@ export const dashboardMetrics = () => '/dashboard/metrics';
export const dashboardSelected = (id = ':dashboardId', hash) => hashed(`/dashboard/${ id }`, hash); export const dashboardSelected = (id = ':dashboardId', hash) => hashed(`/dashboard/${ id }`, hash);
export const dashboardMetricDetails = (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 dashboardMetricCreate = (dashboardId = ':dashboardId', hash) => hashed(`/dashboard/${ dashboardId }/metric/create`, hash);
export const metricCreate = () => `/metric/create`; export const metrics = () => `/metrics`;
export const metricDetails = (id = ':metricId', hash) => hashed(`/metric/${ id }`, hash); export const metricCreate = () => `/metrics/create`;
export const metricDetails = (id = ':metricId', hash) => hashed(`/metrics/${ id }`, hash);
export const RESULTS_QUERY_KEY = 'results'; export const RESULTS_QUERY_KEY = 'results';
@ -120,12 +121,16 @@ const REQUIRED_SITE_ID_ROUTES = [
session(''), session(''),
sessions(), sessions(),
assist(), assist(),
metrics(),
metricDetails(''),
dashboard(''), dashboard(''),
dashboardSelected(''), dashboardSelected(''),
dashboardMetrics(''), dashboardMetrics(''),
// dashboardMetricCreate(''), dashboardMetricCreate(''),
dashboardMetricDetails(''), dashboardMetricDetails(''),
metricCreate(''),
error(''), error(''),
errors(), errors(),
onboarding(''), onboarding(''),
@ -158,8 +163,7 @@ const SITE_CHANGE_AVALIABLE_ROUTES = [
sessions(), sessions(),
assist(), assist(),
dashboard(), dashboard(),
dashboardMetrics(''), metrics(),
dashboardSelected(''),
errors(), errors(),
onboarding('') onboarding('')
]; ];

View file

@ -3,7 +3,7 @@ import APIClient from 'App/api_client';
import { IWidget } from "App/components/Dashboard/store/widget"; import { IWidget } from "App/components/Dashboard/store/widget";
export interface IDashboardService { export interface IDashboardService {
initClient(): void initClient(client?: APIClient)
getWidgets(dashboardId: string): Promise<any> getWidgets(dashboardId: string): Promise<any>
getDashboards(): Promise<any[]> getDashboards(): Promise<any[]>
@ -20,15 +20,15 @@ export interface IDashboardService {
} }
export class DashboardService implements IDashboardService { export default class DashboardService implements IDashboardService {
private client: APIClient; private client: APIClient;
constructor(client?: APIClient) { constructor(client?: APIClient) {
this.client = client ? client : new APIClient(); this.client = client ? client : new APIClient();
} }
initClient() { initClient(client?: APIClient) {
this.client = new APIClient(); this.client = client || new APIClient();
} }
/** /**
@ -102,8 +102,8 @@ export class DashboardService implements IDashboardService {
saveMetric(metric: IWidget, dashboardId?: string): Promise<any> { saveMetric(metric: IWidget, dashboardId?: string): Promise<any> {
const data = metric.toJson(); const data = metric.toJson();
const path = dashboardId ? `/metrics` : '/metrics'; // TODO change to /dashboards/:dashboardId/widgets // const path = dashboardId ? `/metrics` : '/metrics'; // TODO change to /dashboards/:dashboardId/widgets
// const path = dashboardId ? `/dashboards/${dashboardId}/widgets` : '/widgets'; const path = dashboardId ? `/dashboards/${dashboardId}/metrics` : '/metrics';
if (metric.widgetId) { if (metric.widgetId) {
return this.client.put(path + '/' + metric.widgetId, data) return this.client.put(path + '/' + metric.widgetId, data)
} else { } else {

View file

@ -0,0 +1,91 @@
import Widget, { IWidget } from "App/mstore/types/widget";
import APIClient from 'App/api_client';
export interface IMetricService {
initClient(client?: APIClient): void;
getMetrics(): Promise<any>;
getMetric(metricId: string): Promise<any>;
saveMetric(metric: IWidget, dashboardId?: string): Promise<any>;
deleteMetric(metricId: string): Promise<any>;
getTemplates(): Promise<any>;
}
export default class MetricService implements IMetricService {
private client: APIClient;
constructor(client?: APIClient) {
this.client = client ? client : new APIClient();
}
initClient(client?: APIClient) {
this.client = client || new APIClient();
}
/**
* Get all metrics.
* @returns {Promise<any>}
*/
getMetrics(): Promise<any> {
return this.client.get('/metrics')
.then(response => response.json())
.then(response => response.data || []);
}
/**
* Get a metric by metricId.
* @param metricId
* @returns {Promise<any>}
*/
getMetric(metricId: string): Promise<any> {
return this.client.get('/metrics/' + metricId)
.then(response => response.json())
.then(response => response.data || {});
}
/**
* Save a metric.
* @param metric
* @returns
*/
saveMetric(metric: IWidget, dashboardId?: string): Promise<any> {
const data = metric.toJson()
const isCreating = !data[Widget.ID_KEY];
const method = isCreating ? 'post' : 'put';
if(dashboardId) {
const url = `/dashboards/${dashboardId}/metrics`;
return this.client[method](url, data)
.then(response => response.json())
.then(response => response.data || {});
} else {
const url = isCreating ? '/metrics' : '/metrics/' + data[Widget.ID_KEY];
return this.client[method](url, data)
.then(response => response.json())
.then(response => response.data || {});
}
}
/**
* Delete a metric.
* @param metricId
* @returns {Promise<any>}
*/
deleteMetric(metricId: string): Promise<any> {
return this.client.delete('/metrics/' + metricId)
.then(response => response.json())
.then(response => response.data);
}
/**
* Get all templates.
* @returns {Promise<any>}
*/
getTemplates(): Promise<any> {
return this.client.get('/metrics/templates')
.then(response => response.json())
.then(response => response.data || []);
}
}

View file

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