feat(ui) - dashboard - wip
This commit is contained in:
parent
7796ff8a6c
commit
1549c554a7
38 changed files with 1473 additions and 280 deletions
|
|
@ -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>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './DashboardRouter';
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './MetricListItem';
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './WidgetChart'
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export { default } from './dashboardStore'
|
export { default as DashboardStore } from './dashboardStore';
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
341
frontend/app/mstore/dashboardStore.ts
Normal file
341
frontend/app/mstore/dashboardStore.ts
Normal 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),
|
||||||
|
]
|
||||||
|
|
@ -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()} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
177
frontend/app/mstore/metricStore.ts
Normal file
177
frontend/app/mstore/metricStore.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
152
frontend/app/mstore/types/dashboard.ts
Normal file
152
frontend/app/mstore/types/dashboard.ts
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
61
frontend/app/mstore/types/filter.ts
Normal file
61
frontend/app/mstore/types/filter.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
66
frontend/app/mstore/types/filterItem.ts
Normal file
66
frontend/app/mstore/types/filterItem.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
38
frontend/app/mstore/types/filterSeries.ts
Normal file
38
frontend/app/mstore/types/filterSeries.ts
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
frontend/app/mstore/types/widget.ts
Normal file
137
frontend/app/mstore/types/widget.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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('')
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
91
frontend/app/services/MetricService.ts
Normal file
91
frontend/app/services/MetricService.ts
Normal 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 || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
Loading…
Add table
Reference in a new issue