diff --git a/frontend/app/Router.js b/frontend/app/Router.js
index 89fbdd343..665ff4df6 100644
--- a/frontend/app/Router.js
+++ b/frontend/app/Router.js
@@ -14,7 +14,8 @@ 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/Dashboard';
+import DashboardPure from 'Components/Dashboard/NewDashboard';
+import WidgetViewPure from 'Components/Dashboard/WidgetView';
import ErrorsPure from 'Components/Errors/Errors';
import Header from 'Components/Header/Header';
// import ResultsModal from 'Shared/Results/ResultsModal';
@@ -35,6 +36,7 @@ import { setSessionPath } from 'Duck/sessions';
const BugFinder = withSiteIdUpdater(BugFinderPure);
const Dashboard = withSiteIdUpdater(DashboardPure);
+const WidgetView = withSiteIdUpdater(WidgetViewPure);
const Session = withSiteIdUpdater(SessionPure);
const LiveSession = withSiteIdUpdater(LiveSessionPure);
const Assist = withSiteIdUpdater(AssistPure);
@@ -46,7 +48,8 @@ const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails);
const withSiteId = routes.withSiteId;
const withObTab = routes.withObTab;
-const DASHBOARD_PATH = routes.dashboard();
+const DASHBOARD_PATH = routes.dashboardSelected();
+const WIDGET_PATAH = routes.dashboardMetric();
const SESSIONS_PATH = routes.sessions();
const ASSIST_PATH = routes.assist();
const ERRORS_PATH = routes.errors();
@@ -180,6 +183,7 @@ class Router extends React.Component {
}
+
diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js
index a42f19468..7a725dd31 100644
--- a/frontend/app/api_client.js
+++ b/frontend/app/api_client.js
@@ -1,5 +1,4 @@
import store from 'App/store';
-
import { queried } from './routes';
const siteIdRequiredPaths = [
diff --git a/frontend/app/components/Dashboard/NewDashboard.tsx b/frontend/app/components/Dashboard/NewDashboard.tsx
new file mode 100644
index 000000000..252e8bc53
--- /dev/null
+++ b/frontend/app/components/Dashboard/NewDashboard.tsx
@@ -0,0 +1,45 @@
+import React, { useEffect } from 'react';
+import { Switch, Route, Redirect } from 'react-router';
+import withPageTitle from 'HOCs/withPageTitle';
+import { observer } from "mobx-react-lite";
+import { withDashboardStore } from './store/store';
+import { withRouter } from 'react-router-dom';
+import DashboardView from './components/DashboardView';
+import { dashboardSelected, dashboardMetric, withSiteId } from 'App/routes';
+
+function NewDashboard(props) {
+ const { store, match: { params: { siteId, dashboardId, metricId } } } = props;
+ const dashboard = store.selectedDashboard;
+
+ useEffect(() => {
+ store.setSiteId(siteId);
+ if (dashboardId) {
+ store.selectDashboardById(dashboardId);
+ } else {
+ store.selectDefaultDashboard();
+ }
+ }, [dashboardId]);
+
+ return (
+
+
+ MENU
+
+
+
+
+
+
+
+ Metric
+
+
+
+
+
+ );
+}
+
+export default withPageTitle('New Dashboard')(
+ withRouter(withDashboardStore(observer(NewDashboard)))
+);
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/WidgetView/WidgetView.tsx
new file mode 100644
index 000000000..6b2e10805
--- /dev/null
+++ b/frontend/app/components/Dashboard/WidgetView/WidgetView.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+function WidgetView(props) {
+ return (
+
+ Widget view
+
+ );
+}
+
+export default WidgetView;
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/WidgetView/index.ts b/frontend/app/components/Dashboard/WidgetView/index.ts
new file mode 100644
index 000000000..7bafa7a72
--- /dev/null
+++ b/frontend/app/components/Dashboard/WidgetView/index.ts
@@ -0,0 +1 @@
+export { default } from './WidgetView'
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/WidgetWrapper/WidgetWrapper.tsx b/frontend/app/components/Dashboard/WidgetWrapper/WidgetWrapper.tsx
new file mode 100644
index 000000000..35e088d52
--- /dev/null
+++ b/frontend/app/components/Dashboard/WidgetWrapper/WidgetWrapper.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { observer } from "mobx-react-lite";
+import { useDashboardStore } from '../store/store';
+import cn from 'classnames';
+import { Link } from 'UI';
+import { dashboardMetric, withSiteId } from 'App/routes';
+
+function WidgetWrapper(props) {
+ const { widget } = props;
+ const store: any = useDashboardStore();
+ const dashboard = store.selectedDashboard;
+ const siteId = store.siteId;
+
+ return (
+
+
+
+ {widget.name} - {widget.position}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default observer(WidgetWrapper);
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/WidgetWrapper/index.ts b/frontend/app/components/Dashboard/WidgetWrapper/index.ts
new file mode 100644
index 000000000..83890df93
--- /dev/null
+++ b/frontend/app/components/Dashboard/WidgetWrapper/index.ts
@@ -0,0 +1 @@
+export { default } from './WidgetWrapper';
\ 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
new file mode 100644
index 000000000..850726789
--- /dev/null
+++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import WidgetWrapper from '../../WidgetWrapper';
+import { observer } from 'mobx-react-lite';
+import { withDashboardStore } from '../../store/store';
+
+function DashboardView(props) {
+ const { store } = props;
+ const dashboard = store.selectedDashboard
+ const list = dashboard?.widgets;
+ return dashboard ? (
+
+ test {dashboard.dashboardId}
+
+ {list && list.map(item => )}
+
+
+ ) : Loading...
;
+}
+
+export default withDashboardStore(observer(DashboardView));
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/components/DashboardView/index.ts b/frontend/app/components/Dashboard/components/DashboardView/index.ts
new file mode 100644
index 000000000..569832baa
--- /dev/null
+++ b/frontend/app/components/Dashboard/components/DashboardView/index.ts
@@ -0,0 +1 @@
+export { default } from './DashboardView'
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/store/dashboard.ts b/frontend/app/components/Dashboard/store/dashboard.ts
new file mode 100644
index 000000000..9395b7c1b
--- /dev/null
+++ b/frontend/app/components/Dashboard/store/dashboard.ts
@@ -0,0 +1,115 @@
+import { makeAutoObservable, makeObservable, observable, action, runInAction, computed, reaction } from "mobx"
+import Widget from "./widget"
+// import APIClient from 'App/api_client';
+
+export default class Dashboard {
+ dashboardId: any = undefined
+ name: string = "New Dashboard"
+ isPriavte: boolean = false
+ widgets: Widget[] = []
+ isValid: boolean = false
+ isPinned: boolean = false
+
+ constructor() {
+ makeAutoObservable(this, {
+ name: observable,
+ isPriavte: 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,
+ })
+ }
+
+ toJson() {
+ return {
+ dashboardId: this.dashboardId,
+ name: this.name,
+ isPrivate: this.isPriavte,
+ widgets: this.widgets.map(w => w.toJson())
+ }
+ }
+
+ fromJson(json: any) {
+ runInAction(() => {
+ this.dashboardId = json.dashboardId
+ this.name = json.name
+ this.isPriavte = json.isPrivate
+ this.widgets = json.widgets.map(w => new Widget().fromJson(w))
+ })
+ return this
+ }
+
+ validate() {
+ this.isValid = this.name.length > 0
+ }
+
+ addWidget(widget: Widget) {
+ this.widgets.push(widget)
+ }
+
+ removeWidget(widgetId: string) {
+ this.widgets = this.widgets.filter(w => w.widgetId !== widgetId)
+ }
+
+ updateWidget(widget: Widget) {
+ 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) {
+ 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/components/Dashboard/store/dashboardStore.ts b/frontend/app/components/Dashboard/store/dashboardStore.ts
new file mode 100644
index 000000000..1227b5611
--- /dev/null
+++ b/frontend/app/components/Dashboard/store/dashboardStore.ts
@@ -0,0 +1,233 @@
+import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
+import Dashboard from "./dashboard"
+import APIClient from 'App/api_client';
+import Widget from "./widget";
+
+export default class DashboardStore {
+ dashboards: Dashboard[] = []
+ selectedDashboard: Dashboard | null = null
+ isLoading: boolean = false
+ siteId: any = null
+
+ private client = new APIClient()
+
+ constructor() {
+ makeAutoObservable(this, {
+ dashboards: observable,
+ selectedDashboard: observable,
+ isLoading: observable,
+
+ addDashboard: action,
+ removeDashboard: action,
+ updateDashboard: action,
+ getDashboard: action,
+ getDashboardIndex: action,
+ getDashboardByIndex: action,
+ getDashboardCount: action,
+ getDashboardIndexByDashboardId: action,
+ selectDashboardById: action,
+ toJson: action,
+ fromJson: action,
+ setSiteId: action,
+ })
+
+
+ // TODO remove this sample data
+ this.dashboards = sampleDashboards
+ this.selectedDashboard = sampleDashboards[0]
+
+ // setInterval(() => {
+ // this.selectedDashboard?.addWidget(getRandomWidget())
+ // }, 3000)
+
+ // setInterval(() => {
+ // this.selectedDashboard?.widgets[4].update({ position: 2 })
+ // this.selectedDashboard?.swapWidgetPosition(2, 0)
+ // }, 3000)
+ }
+
+ fetchList() {
+ this.isLoading = true
+
+ this.client.get('/dashboards')
+ .then(response => {
+ runInAction(() => {
+ this.dashboards = response.data.map(d => new Dashboard().fromJson(d))
+ this.isLoading = false
+ })
+ }
+ )
+ }
+
+ fetch(dashboardId: string) {
+ this.isLoading = true
+ this.client.get(`/dashboards/${dashboardId}`)
+ .then(response => {
+ runInAction(() => {
+ this.selectedDashboard = new Dashboard().fromJson(response.data)
+ this.isLoading = false
+ })
+ }
+ )
+ }
+
+ save(dashboard: Dashboard) {
+ dashboard.validate()
+ if (dashboard.isValid) {
+ this.isLoading = true
+ if (dashboard.dashboardId) {
+ this.client.put(`/dashboards/${dashboard.dashboardId}`, dashboard.toJson())
+ .then(response => {
+ runInAction(() => {
+ this.isLoading = false
+ })
+ }
+ )
+ } else {
+ this.client.post('/dashboards', dashboard.toJson())
+ .then(response => {
+ runInAction(() => {
+ this.isLoading = false
+ })
+ }
+ )
+ }
+ } else {
+ alert("Invalid dashboard") // TODO show validation errors
+ }
+ }
+
+ saveDashboardWidget(dashboard: Dashboard, widget: Widget) {
+ widget.validate()
+ if (widget.isValid) {
+ this.isLoading = true
+ if (widget.widgetId) {
+ this.client.put(`/dashboards/${dashboard.dashboardId}/widgets/${widget.widgetId}`, widget.toJson())
+ .then(response => {
+ runInAction(() => {
+ this.isLoading = false
+ })
+ }
+ )
+ } else {
+ this.client.post(`/dashboards/${dashboard.dashboardId}/widgets`, widget.toJson())
+ .then(response => {
+ runInAction(() => {
+ this.isLoading = false
+ })
+ }
+ )
+ }
+ }
+ }
+
+ delete(dashboard: Dashboard) {
+ this.isLoading = true
+ this.client.delete(`/dashboards/${dashboard.dashboardId}`)
+ .then(response => {
+ runInAction(() => {
+ this.isLoading = false
+ })
+ }
+ )
+ }
+
+ 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
+ }
+
+ initDashboard(dashboard: Dashboard | null) {
+ this.selectedDashboard = dashboard || new Dashboard()
+ }
+
+ 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) || null;;
+ }
+
+ setSiteId = (siteId: any) => {
+ this.siteId = siteId
+ }
+
+ selectDefaultDashboard = () => {
+ const pinnedDashboard = this.dashboards.find(d => d.isPinned)
+ if (pinnedDashboard) {
+ this.selectedDashboard = pinnedDashboard
+ } else {
+ this.selectedDashboard = this.dashboards[0]
+ }
+ }
+}
+
+function getRandomWidget() {
+ const widget = new Widget();
+ widget.widgetId = Math.floor(Math.random() * 100);
+ widget.name = "Widget " + Math.floor(Math.random() * 100);
+ widget.type = "random";
+ widget.colSpan = Math.floor(Math.random() * 2) + 1;
+ return widget;
+}
+
+function getRandomDashboard(id: any = null) {
+ const dashboard = new Dashboard();
+ dashboard.name = "Random Dashboard";
+ dashboard.dashboardId = id ? id : "random-dashboard-" + Math.floor(Math.random() * 10);
+ for (let i = 0; i < 10; i++) {
+ const widget = getRandomWidget();
+ widget.position = i;
+ dashboard.addWidget(widget);
+ }
+ return dashboard;
+}
+
+const sampleDashboards = [
+ getRandomDashboard(12),
+ getRandomDashboard(),
+ getRandomDashboard(),
+ getRandomDashboard(),
+ getRandomDashboard(),
+ getRandomDashboard(),
+]
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/store/index.ts b/frontend/app/components/Dashboard/store/index.ts
new file mode 100644
index 000000000..bc920972d
--- /dev/null
+++ b/frontend/app/components/Dashboard/store/index.ts
@@ -0,0 +1 @@
+export { default } from './dashboardStore'
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/store/store.tsx b/frontend/app/components/Dashboard/store/store.tsx
new file mode 100644
index 000000000..64e1944b9
--- /dev/null
+++ b/frontend/app/components/Dashboard/store/store.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+
+const StoreContext = React.createContext(null)
+
+export const DashboardStoreProvider = ({ children, store }) => {
+ return (
+ {children}
+ );
+};
+
+export const useDashboardStore = () => React.useContext(StoreContext);
+
+export const withDashboardStore = (Component) => (props) => {
+ return ;
+};
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/store/widget.ts b/frontend/app/components/Dashboard/store/widget.ts
new file mode 100644
index 000000000..6326dd6db
--- /dev/null
+++ b/frontend/app/components/Dashboard/store/widget.ts
@@ -0,0 +1,61 @@
+import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
+
+export default class Widget {
+ widgetId: any = undefined
+ name: string = ""
+ type: string = ""
+ position: number = 0
+ data: any = {}
+ isLoading: boolean = false
+ isValid: boolean = false
+ dashboardId: any = undefined
+ colSpan: number = 2
+
+ constructor() {
+ makeAutoObservable(this, {
+ widgetId: observable,
+ name: observable,
+ type: observable,
+ position: observable,
+ data: observable,
+ isLoading: observable,
+ isValid: observable,
+ dashboardId: observable,
+ colSpan: observable,
+
+ fromJson: action,
+ toJson: action,
+ validate: action,
+ update: action,
+ })
+ }
+
+ fromJson(json: any) {
+ runInAction(() => {
+ this.widgetId = json.widgetId
+ this.name = json.name
+ this.type = json.type
+ this.data = json.data
+ })
+ return this
+ }
+
+ toJson() {
+ return {
+ widgetId: this.widgetId,
+ name: this.name,
+ type: this.type,
+ data: this.data
+ }
+ }
+
+ validate() {
+ this.isValid = this.name.length > 0
+ }
+
+ update(data: any) {
+ runInAction(() => {
+ Object.assign(this, data)
+ })
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/components/Header/Header.js b/frontend/app/components/Header/Header.js
index 6d541fca7..9b8819cdc 100644
--- a/frontend/app/components/Header/Header.js
+++ b/frontend/app/components/Header/Header.js
@@ -104,7 +104,7 @@ const Header = (props) => {
className={ styles.nav }
activeClassName={ styles.active }
>
- { 'Metrics' }
+ { 'Dashboard' }
diff --git a/frontend/app/initialize.js b/frontend/app/initialize.js
index 60760504f..96a527f14 100644
--- a/frontend/app/initialize.js
+++ b/frontend/app/initialize.js
@@ -5,12 +5,18 @@ import { Provider } from 'react-redux';
import store from './store';
import Router from './Router';
+import DashboardStore from './components/Dashboard/store';
+import { DashboardStoreProvider } from './components/Dashboard/store/store';
+
document.addEventListener('DOMContentLoaded', () => {
+ const dashboardStore = new DashboardStore();
render(
(
-
+
+
+
),
document.getElementById('app'),
diff --git a/frontend/app/routes.js b/frontend/app/routes.js
index f9987d49c..3ee5b6a41 100644
--- a/frontend/app/routes.js
+++ b/frontend/app/routes.js
@@ -100,7 +100,9 @@ export const testBuilderNew = () => '/test-builder';
export const testBuilder = (testId = ':testId') => `/test-builder/${ testId }`;
-export const dashboard = () => '/metrics';
+export const dashboard = () => '/dashboard';
+export const dashboardSelected = (id = ':dashboardId', hash) => hashed(`/dashboard/${ id }`, hash);
+export const dashboardMetric = (id = ':dashboardId', metricId = ':metricId', hash) => hashed(`/dashboard/${ id }/metric/${metricId}`, hash);
export const RESULTS_QUERY_KEY = 'results';
export const METRICS_QUERY_KEY = 'metrics';