diff --git a/frontend/app/Router.js b/frontend/app/Router.js index b8c7f7a03..a31391127 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -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 &&
} - - - - { - const client = new APIClient(jwt); - switch (location.pathname) { - case '/integrations/slack': - client.post('integrations/slack/add', { - code: location.search.split('=')[ 1 ], - state: tenantId, - }); - break; + }> + + + + { + 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 ; } - return ; } - } - /> - { onboarding && - - } - { siteIdList.length === 0 && - - } - - - - - - - {/* - - - - */} + /> + { onboarding && + + } + { siteIdList.length === 0 && + + } + + + - - - - - - - - - } /> - { routes.redirects.map(([ fr, to ]) => ( - - )) } - + + + + + + + {/* + + + + */} + + + + + + + + + + } /> + { routes.redirects.map(([ fr, to ]) => ( + + )) } + + + + + : + }> + + + + { !existingTenant && } + - : - - - - { !existingTenant && } - - ; + ; } } diff --git a/frontend/app/components/Dashboard/NewDashboard.tsx b/frontend/app/components/Dashboard/NewDashboard.tsx index 832af0df0..56395f8a9 100644 --- a/frontend/app/components/Dashboard/NewDashboard.tsx +++ b/frontend/app/components/Dashboard/NewDashboard.tsx @@ -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 ( - - -
-
- -
-
- -
+ +
+
+
- - - - - { dashboardId && ( - <> - -
-
- -
-
- -
-
-
- - - {/* - - - */} - {/* */} - - )} - +
+ +
+
+
); } diff --git a/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx b/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx index 49cef58f0..8a5892cb3 100644 --- a/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx +++ b/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx @@ -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) {
Dashboards
{dashboards.map((item: any) => ( - //
- // {item.name} - //
onItemClick(item)} + // onClick={() => onItemClick(item)} // TODO add click handler leading = {(
-
+ {item.isPublic &&
} {item.isPinned &&
}
)} diff --git a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx index d7c7cfa05..90729935f 100644 --- a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx +++ b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx @@ -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(widgetCategories[0]); const [selectedWidgets, setSelectedWidgets] = React.useState([]); const selectedWidgetIds = selectedWidgets.map((widget: any) => widget.widgetId); diff --git a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx new file mode 100644 index 000000000..82ab00a0e --- /dev/null +++ b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx @@ -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 ( +
+ + + + + + + + + + + + + + + + + +
+ ); +} + +export default withRouter(DashboardRouter); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/DashboardRouter/index.ts b/frontend/app/components/Dashboard/components/DashboardRouter/index.ts new file mode 100644 index 000000000..62c27a8fd --- /dev/null +++ b/frontend/app/components/Dashboard/components/DashboardRouter/index.ts @@ -0,0 +1 @@ +export { default } from './DashboardRouter'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx b/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx index 646284d9e..c53a7a378 100644 --- a/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx +++ b/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx @@ -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(, {}); - // }, []); 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(, {}) } - return ( + return useObserver(() => (
{dashboardsPicked.map((item: any) => ( @@ -48,7 +48,7 @@ function DashboardSideMenu(props) { onClick={() => onItemClick(item)} leading = {(
-
+ {item.isPublic &&
} {item.isPinned &&
}
)} @@ -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))} />
@@ -92,7 +92,7 @@ function DashboardSideMenu(props) { />
- ); + )); } -export default withRouter(observer(DashboardSideMenu)); \ No newline at end of file +export default withRouter(DashboardSideMenu); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index c229ae104..4ac3380c6 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -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) {
- +
Right diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index 237d5fcb4..b52a25329 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -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'; diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx new file mode 100644 index 000000000..cafcf53d8 --- /dev/null +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -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 => ( + +
+
·
+ {dashboard.name} +
+ + )) + ); +} + +function MetricListItem(props: Props) { + const { metric } = props; + return ( +
+
+ + {metric.name} + +
+
+
+ + {/*
+
·
+ Dashboards +
*/} +
+
{metric.owner}
+ {/*
+ + {metric.isPublic ? 'Team' : 'Private'} +
*/} +
Last Modified
+
+ ); +} + +export default MetricListItem; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/MetricListItem/index.ts b/frontend/app/components/Dashboard/components/MetricListItem/index.ts new file mode 100644 index 000000000..b4c506a23 --- /dev/null +++ b/frontend/app/components/Dashboard/components/MetricListItem/index.ts @@ -0,0 +1 @@ +export { default } from './MetricListItem'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index 656ce2d3a..9ef809261 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -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(() => ( @@ -28,44 +24,21 @@ function MetricsList(props: Props) {
Type
Dashboards
Owner
-
Visibility & Edit Access
+ {/*
Visibility & Edit Access
*/}
Last Modified
- {list.slice(start, end).map((metric: any) => ( -
-
- - {metric.name} - -
-
-
Dashboards
-
{metric.owner}
-
- {metric.isPrivate ? ( -
- - Private -
- ) : ( -
- - Team -
- )} -
-
Last Modified
-
+ {list.map((metric: any) => ( + ))}
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} />
diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index b8ec01d25..18c1204bb 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -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(() => (
@@ -16,7 +23,7 @@ function MetricsView(props) {
- ); + )); } export default MetricsView; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx new file mode 100644 index 000000000..53f15faf5 --- /dev/null +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -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
Chart
; + } + + if (metricType === 'table') { + return
Table
; + } + + return
Unknown
; + } + return ( +
+ {renderChart()} +
+ ); +} + +export default WidgetChart; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/WidgetChart/index.ts b/frontend/app/components/Dashboard/components/WidgetChart/index.ts new file mode 100644 index 000000000..0ea9108ea --- /dev/null +++ b/frontend/app/components/Dashboard/components/WidgetChart/index.ts @@ -0,0 +1 @@ +export { default } from './WidgetChart' diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 9cd0002dc..373c399c9 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -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 && ( - <> - issue type - - + <> + issue type + + )} {metric.metricType === 'table' && ( @@ -154,9 +170,9 @@ function WidgetForm(props: Props) { Save
- {metric.widgetId && ( + {metric.exists() && ( <> - @@ -172,4 +188,4 @@ function WidgetForm(props: Props) { )); } -export default withRouter(WidgetForm); \ No newline at end of file +export default WidgetForm; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx index 1e39c2c63..11b9e326c 100644 --- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx +++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx @@ -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'; diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx index 4ad9eaa00..a63c58beb 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx @@ -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 ( -
-
-
-

{widget.name}

-
-
setExpanded(!expanded)} - className="flex items-center cursor-pointer select-none" - > - {expanded ? 'Collapse' : 'Expand'} - + 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(() => ( + +
+ +
+
+

{widget.name}

+
+
setExpanded(!expanded)} + className="flex items-center cursor-pointer select-none" + > + {expanded ? 'Collapse' : 'Expand'} + +
+ + { expanded && }
- { expanded && } + +
- - - -
- ); + + )); } -export default withRouter(WidgetView); \ No newline at end of file +export default WidgetView; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/WidgetWrapper/WidgetWrapper.tsx b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx similarity index 85% rename from frontend/app/components/Dashboard/WidgetWrapper/WidgetWrapper.tsx rename to frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx index 86ce18b36..92c3f42fd 100644 --- a/frontend/app/components/Dashboard/WidgetWrapper/WidgetWrapper.tsx +++ b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx @@ -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) {
- {widget.name} - {widget.position} + {widget.name}
- {/* */}
- +
{/* */}
diff --git a/frontend/app/components/Dashboard/WidgetWrapper/index.ts b/frontend/app/components/Dashboard/components/WidgetWrapper/index.ts similarity index 100% rename from frontend/app/components/Dashboard/WidgetWrapper/index.ts rename to frontend/app/components/Dashboard/components/WidgetWrapper/index.ts diff --git a/frontend/app/components/Dashboard/store/dashboard.ts b/frontend/app/components/Dashboard/store/dashboard.ts index d2e4596d5..dd253a6d3 100644 --- a/frontend/app/components/Dashboard/store/dashboard.ts +++ b/frontend/app/components/Dashboard/store/dashboard.ts @@ -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 } diff --git a/frontend/app/components/Dashboard/store/dashboardStore.ts b/frontend/app/components/Dashboard/store/dashboardStore.ts index 8ca3f6c2e..65dbf861b 100644 --- a/frontend/app/components/Dashboard/store/dashboardStore.ts +++ b/frontend/app/components/Dashboard/store/dashboardStore.ts @@ -167,12 +167,12 @@ export default class DashboardStore implements IDashboardSotre { save(dashboard: IDashboard): Promise { 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(() => { diff --git a/frontend/app/components/Dashboard/store/index.ts b/frontend/app/components/Dashboard/store/index.ts index bc920972d..6baa3c043 100644 --- a/frontend/app/components/Dashboard/store/index.ts +++ b/frontend/app/components/Dashboard/store/index.ts @@ -1 +1 @@ -export { default } from './dashboardStore' \ No newline at end of file +export { default as DashboardStore } from './dashboardStore'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/store/widget.ts b/frontend/app/components/Dashboard/store/widget.ts index 481e7c969..5e624dd54 100644 --- a/frontend/app/components/Dashboard/store/widget.ts +++ b/frontend/app/components/Dashboard/store/widget.ts @@ -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" diff --git a/frontend/app/components/ui/BackLink/BackLink.js b/frontend/app/components/ui/BackLink/BackLink.js index db47ae93f..c66be5d15 100644 --- a/frontend/app/components/ui/BackLink/BackLink.js +++ b/frontend/app/components/ui/BackLink/BackLink.js @@ -6,7 +6,7 @@ export default function BackLink ({ className, to, onClick, label, vertical = false, style }) { const children = ( -
+
{ label &&
{ label }
}
diff --git a/frontend/app/initialize.js b/frontend/app/initialize.js index df06dbbbe..9e73942f7 100644 --- a/frontend/app/initialize.js +++ b/frontend/app/initialize.js @@ -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( diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts new file mode 100644 index 000000000..28e7197fb --- /dev/null +++ b/frontend/app/mstore/dashboardStore.ts @@ -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 + fetch(dashboardId: string) + save(dashboard: IDashboard): Promise + 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 + + saveMetric(metric: IWidget, dashboardId?: string): Promise +} +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 { + 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 { + 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 { + 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 => { + 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), +] \ No newline at end of file diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index ce928b399..88f5472c2 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -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 ; + return ; }; - diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts new file mode 100644 index 000000000..e312ee58c --- /dev/null +++ b/frontend/app/mstore/metricStore.ts @@ -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 + 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 + }) + } +} \ No newline at end of file diff --git a/frontend/app/mstore/types/dashboard.ts b/frontend/app/mstore/types/dashboard.ts new file mode 100644 index 000000000..7f3095744 --- /dev/null +++ b/frontend/app/mstore/types/dashboard.ts @@ -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 + } + }) + } +} \ No newline at end of file diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts new file mode 100644 index 000000000..5a7dcc5e9 --- /dev/null +++ b/frontend/app/mstore/types/filter.ts @@ -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 + } +} \ No newline at end of file diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts new file mode 100644 index 000000000..ea56020c5 --- /dev/null +++ b/frontend/app/mstore/types/filterItem.ts @@ -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 + } +} \ No newline at end of file diff --git a/frontend/app/mstore/types/filterSeries.ts b/frontend/app/mstore/types/filterSeries.ts new file mode 100644 index 000000000..c3051e612 --- /dev/null +++ b/frontend/app/mstore/types/filterSeries.ts @@ -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(), + } + } +} \ No newline at end of file diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts new file mode 100644 index 000000000..c55ce2e97 --- /dev/null +++ b/frontend/app/mstore/types/widget.ts @@ -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 + } +} \ No newline at end of file diff --git a/frontend/app/routes.js b/frontend/app/routes.js index 4aea022a4..08303f78b 100644 --- a/frontend/app/routes.js +++ b/frontend/app/routes.js @@ -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('') ]; diff --git a/frontend/app/services/DashboardService.ts b/frontend/app/services/DashboardService.ts index 871fabb3f..1e5e8974f 100644 --- a/frontend/app/services/DashboardService.ts +++ b/frontend/app/services/DashboardService.ts @@ -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 getDashboards(): Promise @@ -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 { 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 { diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts new file mode 100644 index 000000000..99c935d47 --- /dev/null +++ b/frontend/app/services/MetricService.ts @@ -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; + getMetric(metricId: string): Promise; + saveMetric(metric: IWidget, dashboardId?: string): Promise; + deleteMetric(metricId: string): Promise; + + getTemplates(): Promise; +} + +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} + */ + getMetrics(): Promise { + return this.client.get('/metrics') + .then(response => response.json()) + .then(response => response.data || []); + } + + /** + * Get a metric by metricId. + * @param metricId + * @returns {Promise} + */ + getMetric(metricId: string): Promise { + 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 { + 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} + */ + deleteMetric(metricId: string): Promise { + return this.client.delete('/metrics/' + metricId) + .then(response => response.json()) + .then(response => response.data); + } + + + /** + * Get all templates. + * @returns {Promise} + */ + getTemplates(): Promise { + return this.client.get('/metrics/templates') + .then(response => response.json()) + .then(response => response.data || []); + } +} \ No newline at end of file diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index 49ad0eb0c..944877c16 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -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(); \ No newline at end of file