feat(ui) - dashboards wip

This commit is contained in:
Shekar Siri 2022-03-18 13:31:02 +01:00
parent b8bd169839
commit 38b536aae1
17 changed files with 555 additions and 6 deletions

View file

@ -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 {
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
}
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />

View file

@ -1,5 +1,4 @@
import store from 'App/store';
import { queried } from './routes';
const siteIdRequiredPaths = [

View file

@ -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 (
<div className="page-margin container-90">
<div className="side-menu">
MENU
</div>
<div className="side-menu-margined">
<Switch>
<Route exact path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<DashboardView dashboard={dashboard} />
</Route>
<Route exact path={withSiteId(dashboardMetric(dashboardId, metricId), siteId)}>
<h1>Metric</h1>
</Route>
<Redirect to={withSiteId(dashboardSelected(dashboardId), siteId )} />
</Switch>
</div>
</div>
);
}
export default withPageTitle('New Dashboard')(
withRouter(withDashboardStore(observer(NewDashboard)))
);

View file

@ -0,0 +1,11 @@
import React from 'react';
function WidgetView(props) {
return (
<div>
Widget view
</div>
);
}
export default WidgetView;

View file

@ -0,0 +1 @@
export { default } from './WidgetView'

View file

@ -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 (
<div className={cn("border rounded", 'col-span-' + widget.colSpan)} style={{ userSelect: 'none'}}>
<Link to={withSiteId(dashboardMetric(12, widget.widgetId), siteId)}>
<div className="p-3 cursor-pointer bg-white border-b flex items-center justify-between">
{widget.name} - {widget.position}
<div>
<button className="btn btn-sm btn-outline-primary" onClick={() => dashboard.removeWidget(widget.widgetId)}>
remove
</button>
</div>
</div>
<div className="bg-white h-40">
</div>
</Link>
</div>
);
}
export default observer(WidgetWrapper);

View file

@ -0,0 +1 @@
export { default } from './WidgetWrapper';

View file

@ -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 ? (
<div>
test {dashboard.dashboardId}
<div className="grid grid-cols-2 gap-4">
{list && list.map(item => <WidgetWrapper widget={item} key={item.widgetId} />)}
</div>
</div>
) : <h1>Loading...</h1>;
}
export default withDashboardStore(observer(DashboardView));

View file

@ -0,0 +1 @@
export { default } from './DashboardView'

View file

@ -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
}
})
}
}

View file

@ -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(),
]

View file

@ -0,0 +1 @@
export { default } from './dashboardStore'

View file

@ -0,0 +1,15 @@
import React from 'react'
const StoreContext = React.createContext(null)
export const DashboardStoreProvider = ({ children, store }) => {
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
};
export const useDashboardStore = () => React.useContext(StoreContext);
export const withDashboardStore = (Component) => (props) => {
return <Component {...props} store={useDashboardStore()} />;
};

View file

@ -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)
})
}
}

View file

@ -104,7 +104,7 @@ const Header = (props) => {
className={ styles.nav }
activeClassName={ styles.active }
>
<span>{ 'Metrics' }</span>
<span>{ 'Dashboard' }</span>
</NavLink>
<div className={ styles.right }>
<Announcements />

View file

@ -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(
(
<Provider store={ store }>
<Router />
<DashboardStoreProvider store={ dashboardStore }>
<Router />
</DashboardStoreProvider>
</Provider>
),
document.getElementById('app'),

View file

@ -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';