From 38b536aae152e1464009fd56dbf916efce8e5434 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 18 Mar 2022 13:31:02 +0100 Subject: [PATCH] feat(ui) - dashboards wip --- frontend/app/Router.js | 8 +- frontend/app/api_client.js | 1 - .../app/components/Dashboard/NewDashboard.tsx | 45 ++++ .../Dashboard/WidgetView/WidgetView.tsx | 11 + .../components/Dashboard/WidgetView/index.ts | 1 + .../Dashboard/WidgetWrapper/WidgetWrapper.tsx | 34 +++ .../Dashboard/WidgetWrapper/index.ts | 1 + .../DashboardView/DashboardView.tsx | 20 ++ .../components/DashboardView/index.ts | 1 + .../components/Dashboard/store/dashboard.ts | 115 +++++++++ .../Dashboard/store/dashboardStore.ts | 233 ++++++++++++++++++ .../app/components/Dashboard/store/index.ts | 1 + .../app/components/Dashboard/store/store.tsx | 15 ++ .../app/components/Dashboard/store/widget.ts | 61 +++++ frontend/app/components/Header/Header.js | 2 +- frontend/app/initialize.js | 8 +- frontend/app/routes.js | 4 +- 17 files changed, 555 insertions(+), 6 deletions(-) create mode 100644 frontend/app/components/Dashboard/NewDashboard.tsx create mode 100644 frontend/app/components/Dashboard/WidgetView/WidgetView.tsx create mode 100644 frontend/app/components/Dashboard/WidgetView/index.ts create mode 100644 frontend/app/components/Dashboard/WidgetWrapper/WidgetWrapper.tsx create mode 100644 frontend/app/components/Dashboard/WidgetWrapper/index.ts create mode 100644 frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx create mode 100644 frontend/app/components/Dashboard/components/DashboardView/index.ts create mode 100644 frontend/app/components/Dashboard/store/dashboard.ts create mode 100644 frontend/app/components/Dashboard/store/dashboardStore.ts create mode 100644 frontend/app/components/Dashboard/store/index.ts create mode 100644 frontend/app/components/Dashboard/store/store.tsx create mode 100644 frontend/app/components/Dashboard/store/widget.ts 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';