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 { BrowserRouter, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
|
|
@ -5,28 +6,29 @@ import { Notification } from 'UI';
|
|||
import { Loader } from 'UI';
|
||||
import { fetchUserInfo } from 'Duck/user';
|
||||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
||||
import Login from 'Components/Login/Login';
|
||||
import ForgotPassword from 'Components/ForgotPassword/ForgotPassword';
|
||||
import UpdatePassword from 'Components/UpdatePassword/UpdatePassword';
|
||||
import ClientPure from 'Components/Client/Client';
|
||||
import OnboardingPure from 'Components/Onboarding/Onboarding';
|
||||
import SessionPure from 'Components/Session/Session';
|
||||
import LiveSessionPure from 'Components/Session/LiveSession';
|
||||
import AssistPure from 'Components/Assist';
|
||||
import BugFinderPure from 'Components/BugFinder/BugFinder';
|
||||
import DashboardPure from 'Components/Dashboard/NewDashboard';
|
||||
const Login = lazy(() => import('Components/Login/Login'));
|
||||
const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword'));
|
||||
const UpdatePassword = lazy(() => import('Components/UpdatePassword/UpdatePassword'));
|
||||
const SessionPure = lazy(() => import('Components/Session/Session'));
|
||||
const LiveSessionPure = lazy(() => import('Components/Session/LiveSession'));
|
||||
const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
|
||||
const ClientPure = lazy(() => import('Components/Client/Client'));
|
||||
const AssistPure = lazy(() => import('Components/Assist'));
|
||||
const BugFinderPure = lazy(() => import('Components/BugFinder/BugFinder'));
|
||||
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 ErrorsPure from 'Components/Errors/Errors';
|
||||
import Header from 'Components/Header/Header';
|
||||
// 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 fetchSiteList } from 'Duck/site';
|
||||
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
|
||||
import { fetchList as fetchAlerts } from 'Duck/alerts';
|
||||
import { fetchWatchdogStatus } from 'Duck/watchdogs';
|
||||
import { dashboardService } from "App/services";
|
||||
import { withStore } from 'App/mstore'
|
||||
|
||||
import APIClient from './api_client';
|
||||
import * as routes from './routes';
|
||||
|
|
@ -49,9 +51,12 @@ const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails);
|
|||
const withSiteId = routes.withSiteId;
|
||||
const withObTab = routes.withObTab;
|
||||
|
||||
const METRICS_PATH = routes.metrics();
|
||||
const METRICS_DETAILS = routes.metricDetails();
|
||||
|
||||
const DASHBOARD_PATH = routes.dashboard();
|
||||
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 SESSIONS_PATH = routes.sessions();
|
||||
|
|
@ -69,6 +74,7 @@ const CLIENT_PATH = routes.client();
|
|||
const ONBOARDING_PATH = routes.onboarding();
|
||||
const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
|
||||
|
||||
@withStore
|
||||
@withRouter
|
||||
@connect((state) => {
|
||||
const siteId = state.getIn([ 'user', 'siteId' ]);
|
||||
|
|
@ -115,7 +121,8 @@ class Router extends React.Component {
|
|||
fetchInitialData = () => {
|
||||
Promise.all([
|
||||
this.props.fetchUserInfo().then(() => {
|
||||
dashboardService.initClient();
|
||||
const { mstore } = this.props
|
||||
mstore.initClient();
|
||||
this.props.fetchIntegrationVariables()
|
||||
}),
|
||||
this.props.fetchSiteList().then(() => {
|
||||
|
|
@ -161,65 +168,74 @@ class Router extends React.Component {
|
|||
{!hideHeader && <Header key="header"/>}
|
||||
<Notification />
|
||||
|
||||
<Switch key="content" >
|
||||
<Route path={ CLIENT_PATH } component={ Client } />
|
||||
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
|
||||
<Route
|
||||
path="/integrations/"
|
||||
render={
|
||||
({ location }) => {
|
||||
const client = new APIClient(jwt);
|
||||
switch (location.pathname) {
|
||||
case '/integrations/slack':
|
||||
client.post('integrations/slack/add', {
|
||||
code: location.search.split('=')[ 1 ],
|
||||
state: tenantId,
|
||||
});
|
||||
break;
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<Switch key="content" >
|
||||
<Route path={ CLIENT_PATH } component={ Client } />
|
||||
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
|
||||
<Route
|
||||
path="/integrations/"
|
||||
render={
|
||||
({ location }) => {
|
||||
const client = new APIClient(jwt);
|
||||
switch (location.pathname) {
|
||||
case '/integrations/slack':
|
||||
client.post('integrations/slack/add', {
|
||||
code: location.search.split('=')[ 1 ],
|
||||
state: tenantId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
return <Redirect to={ CLIENT_PATH } />;
|
||||
}
|
||||
return <Redirect to={ CLIENT_PATH } />;
|
||||
}
|
||||
}
|
||||
/>
|
||||
{ onboarding &&
|
||||
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
|
||||
}
|
||||
{ siteIdList.length === 0 &&
|
||||
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
|
||||
}
|
||||
|
||||
<Route exact index path={ withSiteId(DASHBOARD_SELECT_PATH, 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 } /> */}
|
||||
/>
|
||||
{ onboarding &&
|
||||
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
|
||||
}
|
||||
{ siteIdList.length === 0 &&
|
||||
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
|
||||
}
|
||||
|
||||
<Route exact strict path={ withSiteId(METRICS_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(METRICS_DETAILS, siteIdList) } component={ Dashboard } />
|
||||
|
||||
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
|
||||
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
|
||||
<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) } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_SELECT_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_METRIC_CREATE_PATH, siteIdList) } component={ Dashboard } />
|
||||
|
||||
|
||||
|
||||
{/* <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } /> */}
|
||||
|
||||
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
|
||||
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
|
||||
<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>
|
||||
</Loader> :
|
||||
<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>;
|
||||
</Suspense>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,84 +1,46 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Switch, Route, Redirect } from 'react-router';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { observer, useObserver } from "mobx-react-lite";
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import DashboardView from './components/DashboardView';
|
||||
import {
|
||||
dashboardSelected,
|
||||
dashboardMetricDetails,
|
||||
dashboardMetricCreate,
|
||||
withSiteId,
|
||||
dashboardMetrics,
|
||||
} from 'App/routes';
|
||||
import DashboardSideMenu from './components/DashboardSideMenu';
|
||||
import WidgetView from './components/WidgetView';
|
||||
import MetricsView from './components/MetricsView';
|
||||
import { Loader } from 'UI';
|
||||
import DashboardRouter from './components/DashboardRouter';
|
||||
|
||||
function NewDashboard(props) {
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const dashboard: any = dashboardStore.selectedDashboard;
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
|
||||
useEffect(() => {
|
||||
dashboardStore.fetchList();
|
||||
dashboardStore.setSiteId(siteId);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboardId) {
|
||||
dashboardStore.selectDashboardById(dashboardId);
|
||||
}
|
||||
if (!dashboardId) {
|
||||
dashboardStore.fetchList().then((resp) => {
|
||||
if (dashboardId) {
|
||||
dashboardStore.selectDashboardById(dashboardId);
|
||||
} else {
|
||||
dashboardStore.selectDefaultDashboard().then((resp: any) => {
|
||||
history.push(withSiteId(dashboardSelected(resp.dashboardId), siteId));
|
||||
dashboardStore.selectDefaultDashboard().then((b) => {
|
||||
if (!history.location.pathname.includes('/metrics')) {
|
||||
history.push(withSiteId(dashboardSelected(b.dashboardId), siteId));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact strict path={withSiteId(dashboardMetrics(), siteId)}>
|
||||
<div className="page-margin container-90">
|
||||
<div className="side-menu">
|
||||
<DashboardSideMenu />
|
||||
</div>
|
||||
<div className="side-menu-margined">
|
||||
<MetricsView />
|
||||
</div>
|
||||
<Loader loading={loading}>
|
||||
<div className="page-margin container-90">
|
||||
<div className="side-menu">
|
||||
<DashboardSideMenu siteId={siteId} />
|
||||
</div>
|
||||
</Route>
|
||||
<Route exact strict path={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}>
|
||||
<WidgetView />
|
||||
</Route>
|
||||
{ dashboardId && (
|
||||
<>
|
||||
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
|
||||
<div className="page-margin container-90">
|
||||
<div className="side-menu">
|
||||
<DashboardSideMenu />
|
||||
</div>
|
||||
<div className="side-menu-margined">
|
||||
<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>
|
||||
<div className="side-menu-margined">
|
||||
<DashboardRouter siteId={siteId} />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
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>
|
||||
{dashboards.map((item: any) => (
|
||||
// <div className="px-4 py-3 hover:bg-gray-lightest cursor-pointer">
|
||||
// {item.name}
|
||||
// </div>
|
||||
<div key={ item.dashboardId } className="px-4">
|
||||
<SideMenuitem
|
||||
key={ item.dashboardId }
|
||||
active={item.dashboardId === activeDashboardId}
|
||||
title={ item.name }
|
||||
iconName={ item.icon }
|
||||
// onClick={() => onItemClick(item)}
|
||||
// onClick={() => onItemClick(item)} // TODO add click handler
|
||||
leading = {(
|
||||
<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>}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import React from 'react';
|
||||
import WidgetWrapper from '../../WidgetWrapper';
|
||||
import { useDashboardStore } from '../../store/store';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import cn from 'classnames';
|
||||
import { Button } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, unSelectCategory }) {
|
||||
|
|
@ -30,7 +28,6 @@ function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds,
|
|||
function DashboardMetricSelection(props) {
|
||||
const { dashboardStore } = useStore();
|
||||
const widgetCategories = dashboardStore?.widgetCategories;
|
||||
const widgetTemplates = dashboardStore?.widgetTemplates;
|
||||
const [activeCategory, setActiveCategory] = React.useState<any>(widgetCategories[0]);
|
||||
const [selectedWidgets, setSelectedWidgets] = React.useState<any>([]);
|
||||
const selectedWidgetIds = selectedWidgets.map((widget: any) => widget.widgetId);
|
||||
|
|
|
|||
|
|
@ -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 { useStore } from 'App/mstore';
|
||||
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 DashbaordListModal from '../DashbaordListModal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
|
||||
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 { history } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
|
||||
const dashboardsPicked = dashboardStore.dashboards.slice(0, SHOW_COUNT);
|
||||
const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT;
|
||||
|
||||
// React.useEffect(() => {
|
||||
// showModal(<DashbaordListModal />, {});
|
||||
// }, []);
|
||||
|
||||
const redirect = (path) => {
|
||||
history.push(path);
|
||||
|
|
@ -27,7 +27,7 @@ function DashboardSideMenu(props) {
|
|||
|
||||
const onItemClick = (dashboard) => {
|
||||
dashboardStore.selectDashboardById(dashboard.dashboardId);
|
||||
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(dashboardStore.siteId));
|
||||
const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(siteId));
|
||||
history.push(path);
|
||||
};
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ function DashboardSideMenu(props) {
|
|||
showModal(<DashboardModal />, {})
|
||||
}
|
||||
|
||||
return (
|
||||
return useObserver(() => (
|
||||
<div>
|
||||
<SideMenuHeader className="mb-4" text="Dashboards" />
|
||||
{dashboardsPicked.map((item: any) => (
|
||||
|
|
@ -48,7 +48,7 @@ function DashboardSideMenu(props) {
|
|||
onClick={() => onItemClick(item)}
|
||||
leading = {(
|
||||
<div className="ml-2 flex items-center">
|
||||
<div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>
|
||||
{item.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>}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -79,7 +79,7 @@ function DashboardSideMenu(props) {
|
|||
id="menu-manage-alerts"
|
||||
title="Metrics"
|
||||
iconName="bar-chart-line"
|
||||
onClick={() => redirect(withSiteId(dashboardMetrics(), dashboardStore.siteId))}
|
||||
onClick={() => redirect(withSiteId(metrics(), siteId))}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t w-full my-2" />
|
||||
|
|
@ -92,7 +92,7 @@ function DashboardSideMenu(props) {
|
|||
/>
|
||||
</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';
|
||||
|
||||
interface Props {
|
||||
|
||||
siteId: number;
|
||||
}
|
||||
function DashboardView(props: Props) {
|
||||
const { siteId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
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">
|
||||
<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>
|
||||
Right
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import WidgetWrapper from '../../WidgetWrapper';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { NoContent, Button, Loader } from 'UI';
|
||||
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 React from 'react';
|
||||
import { Icon, NoContent, Label, Link, Pagination } from 'UI';
|
||||
import { NoContent, Pagination } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { getRE } from 'App/utils';
|
||||
import MetricListItem from '../MetricListItem';
|
||||
|
||||
interface Props { }
|
||||
function MetricsList(props: Props) {
|
||||
const { dashboardStore } = useStore();
|
||||
const widgets = dashboardStore.widgets;
|
||||
const lenth = widgets.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 { metricStore } = useStore();
|
||||
const metrics = useObserver(() => metricStore.metrics);
|
||||
const lenth = metrics.length;
|
||||
|
||||
const totalPages = list.length;
|
||||
const pageSize = dashboardStore.metricsPageSize;
|
||||
const start = (currentPage - 1) * pageSize;
|
||||
const end = currentPage * pageSize;
|
||||
const metricsSearch = useObserver(() => metricStore.metricsSearch);
|
||||
const filterRE = getRE(metricsSearch, 'i');
|
||||
const list = metrics.filter(w => filterRE.test(w.name));
|
||||
|
||||
|
||||
return useObserver(() => (
|
||||
<NoContent show={lenth === 0} icon="exclamation-circle">
|
||||
|
|
@ -28,44 +24,21 @@ function MetricsList(props: Props) {
|
|||
<div>Type</div>
|
||||
<div>Dashboards</div>
|
||||
<div>Owner</div>
|
||||
<div>Visibility & Edit Access</div>
|
||||
{/* <div>Visibility & Edit Access</div> */}
|
||||
<div>Last Modified</div>
|
||||
</div>
|
||||
|
||||
{list.slice(start, end).map((metric: any) => (
|
||||
<div className="grid grid-cols-7 p-3 border-t select-none">
|
||||
<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>
|
||||
{list.map((metric: any) => (
|
||||
<MetricListItem metric={metric} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-center py-6">
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
totalPages={Math.ceil(totalPages / pageSize)}
|
||||
onPageChange={(page) => dashboardStore.updateKey('metricsPage', page)}
|
||||
limit={pageSize}
|
||||
page={metricStore.page}
|
||||
totalPages={Math.ceil(lenth / metricStore.pageSize)}
|
||||
onPageChange={(page) => metricStore.updateKey('page', page)}
|
||||
limit={metricStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,16 @@ import { Button, PageTitle, Icon, Link } from 'UI';
|
|||
import { withSiteId, dashboardMetricCreate } from 'App/routes';
|
||||
import MetricsList from '../MetricsList';
|
||||
import MetricsSearch from '../MetricsSearch';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
function MetricsView(props) {
|
||||
return (
|
||||
const { metricStore } = useStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
return useObserver(() => (
|
||||
<div>
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<PageTitle title="Metrics" className="mr-3" />
|
||||
|
|
@ -16,7 +23,7 @@ function MetricsView(props) {
|
|||
</div>
|
||||
<MetricsList />
|
||||
</div>
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
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 FilterSeries from '../FilterSeries';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
match: any;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function WidgetForm(props: Props) {
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const metric: any = dashboardStore.currentWidget;
|
||||
console.log('WidgetForm params', props.match.params);
|
||||
const { metricStore } = useStore();
|
||||
const metric: any = metricStore.instance;
|
||||
|
||||
const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries');
|
||||
const tableOptions = metricOf.filter(i => i.type === 'table');
|
||||
|
|
@ -24,31 +27,44 @@ function WidgetForm(props: Props) {
|
|||
const isTimeSeries = metric.metricType === 'timeseries';
|
||||
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 }) => {
|
||||
dashboardStore.editWidget({ [ name ]: value });
|
||||
metricStore.merge({ [ name ]: value });
|
||||
|
||||
if (name === 'metricValue') {
|
||||
dashboardStore.editWidget({ metricValue: [value] });
|
||||
metricStore.merge({ metricValue: [value] });
|
||||
}
|
||||
|
||||
if (name === 'metricOf') {
|
||||
if (value === FilterKey.ISSUE) {
|
||||
dashboardStore.editWidget({ metricValue: ['all'] });
|
||||
metricStore.merge({ metricValue: ['all'] });
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'metricType') {
|
||||
if (value === 'timeseries') {
|
||||
dashboardStore.editWidget({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' });
|
||||
metricStore.merge({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' });
|
||||
} else if (value === 'table') {
|
||||
dashboardStore.editWidget({ metricOf: tableOptions[0].value, viewType: 'table' });
|
||||
metricStore.merge({ metricOf: tableOptions[0].value, viewType: 'table' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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(() => (
|
||||
|
|
@ -88,15 +104,15 @@ function WidgetForm(props: Props) {
|
|||
)}
|
||||
|
||||
{metric.metricOf === FilterKey.ISSUE && (
|
||||
<>
|
||||
<span className="mx-3">issue type</span>
|
||||
<DropdownPlain
|
||||
name="metricValue"
|
||||
options={_issueOptions}
|
||||
value={ metric.metricValue[0] }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
<>
|
||||
<span className="mx-3">issue type</span>
|
||||
<DropdownPlain
|
||||
name="metricValue"
|
||||
options={_issueOptions}
|
||||
value={ metric.metricValue[0] }
|
||||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === 'table' && (
|
||||
|
|
@ -154,9 +170,9 @@ function WidgetForm(props: Props) {
|
|||
Save
|
||||
</Button>
|
||||
<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"/>
|
||||
Delete
|
||||
</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 cn from 'classnames';
|
||||
import WidgetWrapper from '../../WidgetWrapper';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Loader, NoContent, SegmentSelection, Icon } from 'UI';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
|
|
@ -11,8 +11,8 @@ interface Props {
|
|||
}
|
||||
function WidgetPreview(props: Props) {
|
||||
const { className = '' } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const metric: any = dashboardStore.currentWidget;
|
||||
const { metricStore } = useStore();
|
||||
const metric: any = metricStore.instance;
|
||||
const isTimeSeries = metric.metricType === 'timeseries';
|
||||
const isTable = metric.metricType === 'table';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,38 +4,65 @@ import { useStore } from 'App/mstore';
|
|||
import WidgetForm from '../WidgetForm';
|
||||
import WidgetPreview from '../WidgetPreview';
|
||||
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 {
|
||||
|
||||
history: any;
|
||||
match: any
|
||||
siteId: any
|
||||
}
|
||||
function WidgetView(props: Props) {
|
||||
const { match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const { dashboardStore } = useStore();
|
||||
const widget = dashboardStore.currentWidget;
|
||||
return (
|
||||
<div className="page-margin container-70 mb-8">
|
||||
<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" />
|
||||
const { metricStore } = useStore();
|
||||
const widget = useObserver(() => metricStore.instance);
|
||||
const loading = useObserver(() => metricStore.isLoading);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (metricId && metricId !== 'create') {
|
||||
metricStore.fetch(metricId).then((metric) => {
|
||||
// metricStore.init(metric)
|
||||
});
|
||||
} else {
|
||||
metricStore.init();
|
||||
}
|
||||
}, [])
|
||||
|
||||
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>
|
||||
|
||||
{ expanded && <WidgetForm onDelete={onBackHandler} {...props}/>}
|
||||
</div>
|
||||
|
||||
{ expanded && <WidgetForm />}
|
||||
<WidgetPreview className="mt-8" />
|
||||
<WidgetSessions className="mt-8" />
|
||||
</div>
|
||||
|
||||
<WidgetPreview className="mt-8" />
|
||||
<WidgetSessions className="mt-8" />
|
||||
</div>
|
||||
);
|
||||
</Loader>
|
||||
));
|
||||
}
|
||||
|
||||
export default withRouter(WidgetView);
|
||||
export default WidgetView;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { useDashboardStore } from '../store/store';
|
||||
import cn from 'classnames';
|
||||
import { ItemMenu } from 'UI';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import WidgetChart from '../WidgetChart';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
|
@ -46,7 +46,7 @@ function WidgetWrapper(props: Props) {
|
|||
style={{
|
||||
userSelect: 'none',
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
borderColor: canDrop && isOver ? '#394EFF' : '#EEE',
|
||||
borderColor: canDrop && isOver ? '#394EFF' : '',
|
||||
}}
|
||||
ref={dragDropRef}
|
||||
>
|
||||
|
|
@ -54,7 +54,7 @@ function WidgetWrapper(props: Props) {
|
|||
<div
|
||||
className="p-3 cursor-move border-b flex items-center justify-between"
|
||||
>
|
||||
{widget.name} - {widget.position}
|
||||
{widget.name}
|
||||
<div>
|
||||
<ItemMenu
|
||||
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 className="h-40">
|
||||
|
||||
<WidgetChart metric={props.widget} />
|
||||
</div>
|
||||
{/* </Link> */}
|
||||
</div>
|
||||
|
|
@ -82,13 +82,13 @@ export default class Dashboard implements IDashboard {
|
|||
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() {
|
||||
console.log('called...')
|
||||
return this.isValid = this.name.length > 0
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,12 +167,12 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
save(dashboard: IDashboard): Promise<any> {
|
||||
this.isSaving = true
|
||||
const isCreating = !dashboard.dashboardId
|
||||
return dashboardService.saveDashboard(dashboard).then(response => {
|
||||
return dashboardService.saveDashboard(dashboard).then(_dashboard => {
|
||||
runInAction(() => {
|
||||
if (isCreating) {
|
||||
this.addDashboard(response.data)
|
||||
this.addDashboard(_dashboard)
|
||||
} else {
|
||||
this.updateDashboard(response.data)
|
||||
this.updateDashboard(_dashboard)
|
||||
}
|
||||
})
|
||||
}).finally(() => {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './dashboardStore'
|
||||
export { default as DashboardStore } from './dashboardStore';
|
||||
|
|
@ -32,6 +32,7 @@ export interface IWidget {
|
|||
update(data: any): void
|
||||
}
|
||||
export default class Widget implements IWidget {
|
||||
public static get ID_KEY():string { return "widgetId" }
|
||||
widgetId: any = undefined
|
||||
name: string = "New Metric"
|
||||
metricType: string = "timeseries"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function BackLink ({
|
|||
className, to, onClick, label, vertical = false, style
|
||||
}) {
|
||||
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" />
|
||||
{ label && <div className="ml-1">{ label }</div> }
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ import { ModalProvider } from './components/Modal';
|
|||
import ModalRoot from './components/Modal/ModalRoot';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend'
|
||||
import { DndProvider } from 'react-dnd'
|
||||
import Modal from 'react-modal';
|
||||
|
||||
Modal.setAppElement('#modal-root');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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 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 {
|
||||
dashboardStore: IDashboardSotre;
|
||||
metricStore: IMetricStore;
|
||||
|
||||
constructor() {
|
||||
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 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 dashboardMetricDetails = (id = ':dashboardId', metricId = ':metricId', hash) => hashed(`/dashboard/${ id }/metric/${metricId}`, hash);
|
||||
export const dashboardMetricCreate = (id = ':dashboardId', hash) => hashed(`/dashboard/${ id }/metric/create`, hash);
|
||||
export const metricCreate = () => `/metric/create`;
|
||||
export const metricDetails = (id = ':metricId', hash) => hashed(`/metric/${ id }`, hash);
|
||||
export const dashboardMetricCreate = (dashboardId = ':dashboardId', hash) => hashed(`/dashboard/${ dashboardId }/metric/create`, hash);
|
||||
export const metrics = () => `/metrics`;
|
||||
export const metricCreate = () => `/metrics/create`;
|
||||
export const metricDetails = (id = ':metricId', hash) => hashed(`/metrics/${ id }`, hash);
|
||||
|
||||
|
||||
export const RESULTS_QUERY_KEY = 'results';
|
||||
|
|
@ -120,12 +121,16 @@ const REQUIRED_SITE_ID_ROUTES = [
|
|||
session(''),
|
||||
sessions(),
|
||||
assist(),
|
||||
|
||||
metrics(),
|
||||
metricDetails(''),
|
||||
|
||||
dashboard(''),
|
||||
dashboardSelected(''),
|
||||
dashboardMetrics(''),
|
||||
// dashboardMetricCreate(''),
|
||||
dashboardMetricCreate(''),
|
||||
dashboardMetricDetails(''),
|
||||
metricCreate(''),
|
||||
|
||||
error(''),
|
||||
errors(),
|
||||
onboarding(''),
|
||||
|
|
@ -158,8 +163,7 @@ const SITE_CHANGE_AVALIABLE_ROUTES = [
|
|||
sessions(),
|
||||
assist(),
|
||||
dashboard(),
|
||||
dashboardMetrics(''),
|
||||
dashboardSelected(''),
|
||||
metrics(),
|
||||
errors(),
|
||||
onboarding('')
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import APIClient from 'App/api_client';
|
|||
import { IWidget } from "App/components/Dashboard/store/widget";
|
||||
|
||||
export interface IDashboardService {
|
||||
initClient(): void
|
||||
initClient(client?: APIClient)
|
||||
getWidgets(dashboardId: string): 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;
|
||||
|
||||
constructor(client?: APIClient) {
|
||||
this.client = client ? client : new APIClient();
|
||||
}
|
||||
|
||||
initClient() {
|
||||
this.client = new APIClient();
|
||||
initClient(client?: APIClient) {
|
||||
this.client = client || new APIClient();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -102,8 +102,8 @@ export class DashboardService implements IDashboardService {
|
|||
saveMetric(metric: IWidget, dashboardId?: string): Promise<any> {
|
||||
const data = metric.toJson();
|
||||
|
||||
const path = dashboardId ? `/metrics` : '/metrics'; // TODO change to /dashboards/:dashboardId/widgets
|
||||
// const path = dashboardId ? `/dashboards/${dashboardId}/widgets` : '/widgets';
|
||||
// const path = dashboardId ? `/metrics` : '/metrics'; // TODO change to /dashboards/:dashboardId/widgets
|
||||
const path = dashboardId ? `/dashboards/${dashboardId}/metrics` : '/metrics';
|
||||
if (metric.widgetId) {
|
||||
return this.client.put(path + '/' + metric.widgetId, data)
|
||||
} 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 metricService: IMetricService = new MetricService();
|
||||
Loading…
Add table
Reference in a new issue