From 7796ff8a6cce06ca10bedef5593cec0cdc112ceb Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 30 Mar 2022 18:08:09 +0200 Subject: [PATCH] feat(ui) - dashboard - wip --- frontend/app/Router.js | 2 + frontend/app/api_client.js | 2 + .../app/components/Dashboard/NewDashboard.tsx | 74 ++++--- .../DashbaordListModal/DashbaordListModal.tsx | 39 ++++ .../components/DashbaordListModal/index.ts | 1 + .../DashboardForm/DashboardForm.tsx | 7 +- .../DashboardMetricSelection.tsx | 35 ++-- .../DashboardModal/DashboardModal.tsx | 25 ++- .../DashboardSideMenu/DashboardSideMenu.tsx | 55 ++++- .../DashboardView/DashboardView.tsx | 25 +-- .../DashboardWidgetGrid.tsx | 12 +- .../components/MetricsList/MetricsList.tsx | 14 +- .../MetricsSearch/MetricsSearch.tsx | 8 +- .../components/WidgetForm/WidgetForm.tsx | 59 ++++-- .../WidgetPreview/WidgetPreview.tsx | 6 +- .../WidgetSessions/WidgetSessions.tsx | 6 +- .../components/WidgetView/WidgetView.tsx | 6 +- .../components/Dashboard/store/dashboard.ts | 42 +++- .../Dashboard/store/dashboardStore.ts | 195 +++++++++++------- .../app/components/Dashboard/store/widget.ts | 40 +++- frontend/app/components/Modal/Modal.js | 23 +-- .../app/components/Modal/ModalOverlay.css | 32 +++ .../app/components/Modal/ModalOverlay.tsx | 13 +- frontend/app/components/Modal/index.tsx | 44 ++++ frontend/app/initialize.js | 21 +- frontend/app/mstore/index.tsx | 24 +++ frontend/app/services/DashboardService.ts | 142 +++++++++++++ frontend/app/services/index.ts | 3 + 28 files changed, 713 insertions(+), 242 deletions(-) create mode 100644 frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx create mode 100644 frontend/app/components/Dashboard/components/DashbaordListModal/index.ts create mode 100644 frontend/app/components/Modal/ModalOverlay.css create mode 100644 frontend/app/components/Modal/index.tsx create mode 100644 frontend/app/mstore/index.tsx create mode 100644 frontend/app/services/DashboardService.ts create mode 100644 frontend/app/services/index.ts diff --git a/frontend/app/Router.js b/frontend/app/Router.js index e270afaba..b8c7f7a03 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -26,6 +26,7 @@ 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 APIClient from './api_client'; import * as routes from './routes'; @@ -114,6 +115,7 @@ class Router extends React.Component { fetchInitialData = () => { Promise.all([ this.props.fetchUserInfo().then(() => { + dashboardService.initClient(); this.props.fetchIntegrationVariables() }), this.props.fetchSiteList().then(() => { diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index 7a725dd31..8fc753c08 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -23,6 +23,8 @@ const siteIdRequiredPaths = [ '/assist', '/heatmaps', '/custom_metrics', + '/dashboards', + '/metrics' // '/custom_metrics/sessions', ]; diff --git a/frontend/app/components/Dashboard/NewDashboard.tsx b/frontend/app/components/Dashboard/NewDashboard.tsx index f4b6aec3e..832af0df0 100644 --- a/frontend/app/components/Dashboard/NewDashboard.tsx +++ b/frontend/app/components/Dashboard/NewDashboard.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { Switch, Route, Redirect } from 'react-router'; import withPageTitle from 'HOCs/withPageTitle'; import { observer } from "mobx-react-lite"; -import { useDashboardStore } from './store/store'; +import { useStore } from 'App/mstore'; import { withRouter } from 'react-router-dom'; import DashboardView from './components/DashboardView'; import { @@ -18,71 +18,67 @@ import MetricsView from './components/MetricsView'; function NewDashboard(props) { const { history, match: { params: { siteId, dashboardId, metricId } } } = props; - const store: any = useDashboardStore(); - const dashboard = store.selectedDashboard; + const { dashboardStore } = useStore(); + const dashboard: any = dashboardStore.selectedDashboard; useEffect(() => { - store.setSiteId(siteId); + dashboardStore.fetchList(); + dashboardStore.setSiteId(siteId); }, []); useEffect(() => { if (dashboardId) { - store.selectDashboardById(dashboardId); + dashboardStore.selectDashboardById(dashboardId); } if (!dashboardId) { if (dashboardId) { - store.selectDashboardById(dashboardId); + dashboardStore.selectDashboardById(dashboardId); } else { - store.selectDefaultDashboard().then((resp) => { + dashboardStore.selectDefaultDashboard().then((resp: any) => { history.push(withSiteId(dashboardSelected(resp.dashboardId), siteId)); }); } } }, []); - // console.log('rendering dashboard', props.match.params); return ( - <> - {/* { dashboard && dashboard.dashboardId && ( */} - - + + +
+
+ +
+
+ +
+
+
+ + + + { dashboardId && ( + <> +
- +
- + + + {/* + - - { dashboardId && ( - <> - -
-
- -
-
- -
-
-
- - - {/* - - - */} - {/* */} - - )} -
- {/* )} */} - +
*/} + {/* */} + + )} +
); } diff --git a/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx b/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx new file mode 100644 index 000000000..49cef58f0 --- /dev/null +++ b/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Modal from 'react-modal'; +import { useStore } from 'App/mstore'; +import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI'; + +function DashbaordListModal(props) { + const { dashboardStore } = useStore(); + const dashboards = dashboardStore.dashboards; + const activeDashboardId = dashboardStore.selectedDashboard?.dashboardId; + return ( +
+
Dashboards
+
+ {dashboards.map((item: any) => ( + //
+ // {item.name} + //
+
+ onItemClick(item)} + leading = {( +
+
+ {item.isPinned &&
} +
+ )} + /> +
+ ))} +
+
+ ); +} + +export default DashbaordListModal; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/DashbaordListModal/index.ts b/frontend/app/components/Dashboard/components/DashbaordListModal/index.ts new file mode 100644 index 000000000..2948a8225 --- /dev/null +++ b/frontend/app/components/Dashboard/components/DashbaordListModal/index.ts @@ -0,0 +1 @@ +export { default } from './DashbaordListModal' \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx b/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx index 9765424eb..9255663e9 100644 --- a/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx +++ b/frontend/app/components/Dashboard/components/DashboardForm/DashboardForm.tsx @@ -3,13 +3,14 @@ import React from 'react'; import { Input } from 'UI'; import { useDashboardStore } from '../../store/store'; import cn from 'classnames'; +import { useStore } from 'App/mstore'; interface Props { } -function DashboardForm(props) { - const store: any = useDashboardStore(); - const dashboard = store.newDashboard; +function DashboardForm(props: Props) { + const { dashboardStore } = useStore(); + const dashboard = dashboardStore.dashboardInstance; const write = ({ target: { value, name } }) => dashboard.update({ [ name ]: value }) const writeRadio = ({ target: { value, name } }) => { diff --git a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx index 08984e96e..d7c7cfa05 100644 --- a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx +++ b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx @@ -4,6 +4,7 @@ import { useDashboardStore } from '../../store/store'; 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 }) { const selectedCategoryWidgetsCount = useObserver(() => { @@ -27,9 +28,9 @@ function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, } function DashboardMetricSelection(props) { - const store: any = useDashboardStore(); - const widgetCategories = store?.widgetCategories; - const widgetTemplates = store?.widgetTemplates; + 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); @@ -73,18 +74,22 @@ function DashboardMetricSelection(props) {
-
-

Errors Tracking

- 12 -
+ {activeCategory && ( + <> +
+

{activeCategory.name}

+ {activeCategory.widgets.length} +
-
- Showing past 7 days data for visual clue -
- - Select All -
-
+
+ Showing past 7 days data for visual clue +
+ + Select All +
+
+ + )}
@@ -104,7 +109,7 @@ function DashboardMetricSelection(props) {
- {activeCategory.widgets.map((widget: any) => ( + {activeCategory && activeCategory.widgets.map((widget: any) => (
store.newDashboard); + const { dashboardStore } = useStore(); + const { hideModal } = useModal(); + const dashbaord = useObserver(() => dashboardStore.dashboardInstance); + const loading = useObserver(() => dashboardStore.isSaving); + + const onSave = () => { + dashboardStore.save(dashbaord).then(hideModal) + } return useObserver(() => ( -
+

Create Dashboard

@@ -20,7 +30,12 @@ function DashboardModal(props) {
-
diff --git a/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx b/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx index 5a02b25b7..646284d9e 100644 --- a/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx +++ b/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx @@ -1,35 +1,51 @@ import { useObserver, observer, useLocalObservable } from 'mobx-react-lite'; import React from 'react'; -import { SideMenuitem, SideMenuHeader, Icon } from 'UI'; -import { withDashboardStore } from '../../store/store'; +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 { useModal } from 'App/components/Modal'; +import DashbaordListModal from '../DashbaordListModal'; +import DashboardModal from '../DashboardModal'; +const SHOW_COUNT = 5; function DashboardSideMenu(props) { - const { store, history } = props; - const { dashboardId } = store.selectedDashboard; + 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); } const onItemClick = (dashboard) => { - store.selectDashboardById(dashboard.dashboardId); - const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(store.siteId)); + dashboardStore.selectDashboardById(dashboard.dashboardId); + const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(dashboardStore.siteId)); history.push(path); }; + const onAddDashboardClick = (e) => { + dashboardStore.initDashboard(); + showModal(, {}) + } + return (
- {store.dashboards.map(item => ( + {dashboardsPicked.map((item: any) => ( onItemClick(item)} - leading = {(
@@ -38,13 +54,32 @@ function DashboardSideMenu(props) { )} /> ))} +
+ {remainingDashboardsCount > 0 && ( +
showModal(, {})} + > + {remainingDashboardsCount} More +
+ )} +
+
+
+ +
redirect(withSiteId(dashboardMetrics(), store.siteId))} + onClick={() => redirect(withSiteId(dashboardMetrics(), dashboardStore.siteId))} />
@@ -60,4 +95,4 @@ function DashboardSideMenu(props) { ); } -export default withDashboardStore(withRouter(observer(DashboardSideMenu))); \ No newline at end of file +export default withRouter(observer(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 ad2381b35..c229ae104 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -1,27 +1,24 @@ -import React, { useEffect } from 'react'; -import WidgetWrapper from '../../WidgetWrapper'; +import React from 'react'; import { observer } from 'mobx-react-lite'; -import { withDashboardStore } from '../../store/store'; +import { useStore } from 'App/mstore'; import { Button, PageTitle, Link } from 'UI'; import { withSiteId, dashboardMetricCreate } from 'App/routes'; import withModal from 'App/components/Modal/withModal'; -import DashboardModal from '../DashboardModal' import DashboardWidgetGrid from '../DashboardWidgetGrid'; -function DashboardView(props) { - // let { handleModal } = React.useContext(ModalContext); - const { store } = props; - const dashboard = store.selectedDashboard - const list = dashboard?.widgets; - useEffect(() => { - // props.showModal(DashboardModal) - }, []) +interface Props { + +} +function DashboardView(props: Props) { + const { dashboardStore } = useStore(); + const dashboard: any = dashboardStore.selectedDashboard + return (
- +
Right @@ -32,4 +29,4 @@ function DashboardView(props) { ) } -export default withDashboardStore(withModal(observer(DashboardView))); \ No newline at end of file +export default withModal(observer(DashboardView)); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index 1b1256793..237d5fcb4 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import { useDashboardStore } from '../../store/store'; +import { useStore } from 'App/mstore'; import WidgetWrapper from '../../WidgetWrapper'; import { NoContent, Button, Loader } from 'UI'; import { useObserver } from 'mobx-react-lite'; -// import { divider } from '../../Filters/filters.css'; function DashboardWidgetGrid(props) { - const store: any = useDashboardStore(); - const loading = store.isLoading; - const dashbaord = store.selectedDashboard; - const list = dashbaord.widgets; + const { dashboardStore } = useStore(); + const loading = useObserver(() => dashboardStore.isLoading); + const dashbaord: any = dashboardStore.selectedDashboard; + const list: any = dashbaord?.widgets; + return useObserver(() => ( store.metricsPage); - const metricsSearch = useObserver(() => store.metricsSearch); + 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 totalPages = list.length; - const pageSize = store.metricsPageSize; + const pageSize = dashboardStore.metricsPageSize; const start = (currentPage - 1) * pageSize; const end = currentPage * pageSize; @@ -64,7 +64,7 @@ function MetricsList(props: Props) { store.updateKey('metricsPage', page)} + onPageChange={(page) => dashboardStore.updateKey('metricsPage', page)} limit={pageSize} debounceRequest={100} /> diff --git a/frontend/app/components/Dashboard/components/MetricsSearch/MetricsSearch.tsx b/frontend/app/components/Dashboard/components/MetricsSearch/MetricsSearch.tsx index 45c209350..ce4bdbe24 100644 --- a/frontend/app/components/Dashboard/components/MetricsSearch/MetricsSearch.tsx +++ b/frontend/app/components/Dashboard/components/MetricsSearch/MetricsSearch.tsx @@ -1,11 +1,11 @@ import { useObserver } from 'mobx-react-lite'; import React from 'react'; -import { useDashboardStore } from '../../store/store'; +import { useStore } from 'App/mstore'; import { Icon } from 'UI'; function MetricsSearch(props) { - const store: any = useDashboardStore(); - const metricsSearch = useObserver(() => store.metricsSearch); + const { dashboardStore } = useStore(); + const metricsSearch = useObserver(() => dashboardStore.metricsSearch); return useObserver(() => ( @@ -16,7 +16,7 @@ function MetricsSearch(props) { name="metricsSearch" className="bg-white p-2 border rounded w-full pl-10" placeholder="Filter by title, type, dashboard and owner" - onChange={({ target: { name, value } }) => store.updateKey(name, value)} + onChange={({ target: { name, value } }) => dashboardStore.updateKey(name, value)} />
)); diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 631a3b791..9cd0002dc 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -2,20 +2,21 @@ import React from 'react'; import DropdownPlain from 'Shared/DropdownPlain'; import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; -import { useDashboardStore } from '../../store/store'; +import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; import { HelpText, Button, Icon } from 'UI' import FilterSeries from '../FilterSeries'; +import { withRouter } from 'react-router-dom'; interface Props { - // metric: any, - // editWidget: (metric, shouldFetch?) => void + history: any; + match: any; } function WidgetForm(props: Props) { - // const { metric } = props; - const store: any = useDashboardStore(); - const metric = store.currentWidget; + const { history, match: { params: { siteId, dashboardId, metricId } } } = props; + const { dashboardStore } = useStore(); + const metric: any = dashboardStore.currentWidget; const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries'); const tableOptions = metricOf.filter(i => i.type === 'table'); @@ -23,28 +24,32 @@ function WidgetForm(props: Props) { const isTimeSeries = metric.metricType === 'timeseries'; const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions); - const write = ({ target: { value, name } }) => store.editWidget({ [ name ]: value }, false); + const write = ({ target: { value, name } }) => dashboardStore.editWidget({ [ name ]: value }); const writeOption = (e, { value, name }) => { - store.editWidget({ [ name ]: value }, false); + dashboardStore.editWidget({ [ name ]: value }); if (name === 'metricValue') { - store.editWidget({ metricValue: [value] }, false); + dashboardStore.editWidget({ metricValue: [value] }); } if (name === 'metricOf') { if (value === FilterKey.ISSUE) { - store.editWidget({ metricValue: ['all'] }, false); + dashboardStore.editWidget({ metricValue: ['all'] }); } } if (name === 'metricType') { if (value === 'timeseries') { - store.editWidget({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }, false); + dashboardStore.editWidget({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }); } else if (value === 'table') { - store.editWidget({ metricOf: tableOptions[0].value, viewType: 'table' }, false); + dashboardStore.editWidget({ metricOf: tableOptions[0].value, viewType: 'table' }); } } }; + + const onSave = () => { + dashboardStore.saveMetric(metric, dashboardId); + } return useObserver(() => (
@@ -141,20 +146,30 @@ function WidgetForm(props: Props) {
- +
- - + {metric.widgetId && ( + <> + + + + )}
)); } -export default WidgetForm; \ No newline at end of file +export default withRouter(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 12ec3113b..1e39c2c63 100644 --- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx +++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx @@ -1,7 +1,7 @@ import React from 'react'; import cn from 'classnames'; import WidgetWrapper from '../../WidgetWrapper'; -import { useDashboardStore } from '../../store/store'; +import { useStore } from 'App/mstore'; import { Loader, NoContent, SegmentSelection, Icon } from 'UI'; import DateRange from 'Shared/DateRange'; import { useObserver } from 'mobx-react-lite'; @@ -11,8 +11,8 @@ interface Props { } function WidgetPreview(props: Props) { const { className = '' } = props; - const store: any = useDashboardStore(); - const metric = store.currentWidget; + const { dashboardStore } = useStore(); + const metric: any = dashboardStore.currentWidget; const isTimeSeries = metric.metricType === 'timeseries'; const isTable = metric.metricType === 'table'; diff --git a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx index 60eb1a569..cb39f7d3d 100644 --- a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { NoContent } from 'UI'; import cn from 'classnames'; -import { useDashboardStore } from '../../store/store'; +import { useStore } from 'App/mstore'; import SessionItem from 'Shared/SessionItem'; interface Props { @@ -9,8 +9,8 @@ interface Props { } function WidgetSessions(props: Props) { const { className = '' } = props; - const store: any = useDashboardStore(); - const widget = store.currentWidget; + const { dashboardStore } = useStore(); + const widget = dashboardStore.currentWidget; return (
diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx index cbe2d38e4..4ad9eaa00 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { withRouter } from 'react-router-dom'; -import { useDashboardStore } from '../../store/store'; +import { useStore } from 'App/mstore'; import WidgetForm from '../WidgetForm'; import WidgetPreview from '../WidgetPreview'; import WidgetSessions from '../WidgetSessions'; @@ -11,8 +11,8 @@ interface Props { } function WidgetView(props: Props) { const [expanded, setExpanded] = useState(true); - const store: any = useDashboardStore(); - const widget = store.currentWidget; + const { dashboardStore } = useStore(); + const widget = dashboardStore.currentWidget; return (
diff --git a/frontend/app/components/Dashboard/store/dashboard.ts b/frontend/app/components/Dashboard/store/dashboard.ts index dbc6d221f..d2e4596d5 100644 --- a/frontend/app/components/Dashboard/store/dashboard.ts +++ b/frontend/app/components/Dashboard/store/dashboard.ts @@ -1,15 +1,39 @@ import { makeAutoObservable, observable, action, runInAction } from "mobx" -import Widget from "./widget" -// import APIClient from 'App/api_client'; +import Widget, { IWidget } from "./widget" +import { dashboardService } from 'App/services' -export default class Dashboard { +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 { dashboardId: any = undefined name: string = "New Dashboard" isPublic: boolean = false - widgets: Widget[] = [] + widgets: IWidget[] = [] isValid: boolean = false isPinned: boolean = false - currentWidget: Widget = new Widget() + currentWidget: IWidget = new Widget() constructor() { makeAutoObservable(this, { @@ -57,8 +81,8 @@ export default class Dashboard { runInAction(() => { this.dashboardId = json.dashboardId this.name = json.name - this.isPublic = json.isPrivate - this.widgets = json.widgets.map(w => new Widget().fromJson(w)) + this.isPublic = json.isPublic + this.widgets = json.widgets ? json.widgets.map(w => new Widget().fromJson(w)) : [] }) return this } @@ -68,7 +92,7 @@ export default class Dashboard { return this.isValid = this.name.length > 0 } - addWidget(widget: Widget) { + addWidget(widget: IWidget) { this.widgets.push(widget) } @@ -76,7 +100,7 @@ export default class Dashboard { this.widgets = this.widgets.filter(w => w.widgetId !== widgetId) } - updateWidget(widget: Widget) { + updateWidget(widget: IWidget) { const index = this.widgets.findIndex(w => w.widgetId === widget.widgetId) if (index >= 0) { this.widgets[index] = widget diff --git a/frontend/app/components/Dashboard/store/dashboardStore.ts b/frontend/app/components/Dashboard/store/dashboardStore.ts index 8373c3fe9..8ca3f6c2e 100644 --- a/frontend/app/components/Dashboard/store/dashboardStore.ts +++ b/frontend/app/components/Dashboard/store/dashboardStore.ts @@ -1,20 +1,69 @@ import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx" -import Dashboard from "./dashboard" +import Dashboard, { IDashboard } from "./dashboard" import APIClient from 'App/api_client'; -import Widget from "./widget"; -export default class DashboardStore { +import Widget, { IWidget } from "./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(): void + 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() - newDashboard: Dashboard = new Dashboard() - isLoading: boolean = false - siteId: any = null + 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 = false + isSaving: boolean = false private client = new APIClient() @@ -40,25 +89,15 @@ export default class DashboardStore { // TODO remove this sample data - this.dashboards = sampleDashboards - // this.selectedDashboard = sampleDashboards[0] + // this.dashboards = sampleDashboards - // setInterval(() => { - // this.selectedDashboard?.addWidget(getRandomWidget()) - // }, 3000) - - // setInterval(() => { - // this.selectedDashboard?.widgets[4].update({ position: 2 }) - // this.selectedDashboard?.swapWidgetPosition(2, 0) - // }, 3000) - - 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 < 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 = { @@ -79,7 +118,10 @@ export default class DashboardStore { this.widgetCategories.push(cat) } - + } + + initDashboard(dashboard: Dashboard) { + this.dashboardInstance = dashboard || new Dashboard() } updateKey(key: any, value: any) { @@ -90,59 +132,71 @@ export default class DashboardStore { this.currentWidget = new Widget() } - editWidget(widget: Widget) { + editWidget(widget: any) { this.currentWidget.update(widget) } fetchList() { this.isLoading = true - this.client.get('/dashboards') - .then(response => { + dashboardService.getDashboards() + .then((list: any) => { + runInAction(() => { + this.dashboards = list.map(d => new Dashboard().fromJson(d)) + }) + }).finally(() => { 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 - }) - } - ) + dashboardService.getDashboard(dashboardId).then(response => { + runInAction(() => { + this.selectedDashboard = new Dashboard().fromJson(response) + }) + }).finally(() => { + runInAction(() => { + 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 - } + save(dashboard: IDashboard): Promise { + this.isSaving = true + const isCreating = !dashboard.dashboardId + return dashboardService.saveDashboard(dashboard).then(response => { + runInAction(() => { + if (isCreating) { + this.addDashboard(response.data) + } else { + this.updateDashboard(response.data) + } + }) + }).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) { @@ -193,10 +247,6 @@ export default class DashboardStore { return this } - initDashboard(dashboard: Dashboard | null) { - this.selectedDashboard = dashboard || new Dashboard() - } - addDashboard(dashboard: Dashboard) { this.dashboards.push(dashboard) } @@ -234,13 +284,16 @@ export default class DashboardStore { 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 = () => { + selectDefaultDashboard = (): Promise => { return new Promise((resolve, reject) => { if (this.dashboards.length > 0) { const pinnedDashboard = this.dashboards.find(d => d.isPinned) @@ -250,7 +303,11 @@ export default class DashboardStore { this.selectedDashboard = this.dashboards[0] } } - resolve(this.selectedDashboard) + if (this.selectedDashboard) { + resolve(this.selectedDashboard) + } + + reject(new Error("No dashboards found")) }) } } diff --git a/frontend/app/components/Dashboard/store/widget.ts b/frontend/app/components/Dashboard/store/widget.ts index 5b0329cab..481e7c969 100644 --- a/frontend/app/components/Dashboard/store/widget.ts +++ b/frontend/app/components/Dashboard/store/widget.ts @@ -2,7 +2,36 @@ import { makeAutoObservable, runInAction, observable, action, reaction } from "m import Filter from 'Types/filter'; import FilterSeries from "./filterSeries"; -export default class Widget { +export interface IWidget { + widgetId: any + name: string + metricType: string + metricOf: string + metricValue: string + viewType: string + series: FilterSeries[] + sessions: [] + isPublic: boolean + owner: string + lastModified: Date + 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 +} +export default class Widget implements IWidget { widgetId: any = undefined name: string = "New Metric" metricType: string = "timeseries" @@ -11,7 +40,7 @@ export default class Widget { viewType: string = "lineChart" series: FilterSeries[] = [] sessions: [] = [] - isPrivate: boolean = false + isPublic: boolean = false owner: string = "" lastModified: Date = new Date() dashboardIds: any[] = [] @@ -76,7 +105,12 @@ export default class Widget { return { widgetId: this.widgetId, name: this.name, - data: this.data + metricOf: this.metricOf, + metricValue: this.metricValue, + viewType: this.viewType, + series: this.series, + sessions: this.sessions, + isPublic: this.isPublic, } } diff --git a/frontend/app/components/Modal/Modal.js b/frontend/app/components/Modal/Modal.js index af0eff6b3..baf226621 100644 --- a/frontend/app/components/Modal/Modal.js +++ b/frontend/app/components/Modal/Modal.js @@ -1,16 +1,15 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; +import { useModal } from '.'; +import ModalOverlay from './ModalOverlay'; -export default class Modal extends React.PureComponent { - constructor(props) { - super(props); - this.el = document.createElement('div'); - } +export default function Modal({ children }){ + const { component } = useModal(); - render() { - return ReactDOM.createPortal( - this.props.children, - this.el, - ); - } + return component ? ReactDOM.createPortal( + + {component} + , + document.querySelector("#modal-root"), + ) : null; } \ No newline at end of file diff --git a/frontend/app/components/Modal/ModalOverlay.css b/frontend/app/components/Modal/ModalOverlay.css new file mode 100644 index 000000000..e3e33562a --- /dev/null +++ b/frontend/app/components/Modal/ModalOverlay.css @@ -0,0 +1,32 @@ +.overlay { + /* absolute w-full h-screen cursor-pointer */ + position: absolute; + width: 100%; + height: 100vh; + cursor: pointer; + /* transition: all 0.3s ease-in-out; */ + animation: fade 1s forwards; +} +.slide { + position: absolute; + left: -100%; + -webkit-animation: slide 0.5s forwards; + animation: slide 0.5s forwards; +} + +@keyframes fade { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@-webkit-keyframes slide { + 100% { left: 0; } +} + +@keyframes slide { + 100% { left: 0; } +} \ No newline at end of file diff --git a/frontend/app/components/Modal/ModalOverlay.tsx b/frontend/app/components/Modal/ModalOverlay.tsx index 003f29ee6..8660f53a4 100644 --- a/frontend/app/components/Modal/ModalOverlay.tsx +++ b/frontend/app/components/Modal/ModalOverlay.tsx @@ -1,14 +1,19 @@ import React from 'react'; import { ModalContext } from "App/components/Modal/modalContext"; -import useModal from 'App/components/Modal/useModal'; +import { useModal } from 'App/components/Modal'; +import stl from './ModalOverlay.css' function ModalOverlay({ children }) { let modal = useModal(); - // console.log('m', m); return ( -
modal.handleModal(false)} style={{ background: "rgba(0,0,0,0.8)", zIndex: '9999' }}> - {children} +
+
modal.hideModal()} + className={stl.overlay} + style={{ background: "rgba(0,0,0,0.5)" }} + /> +
{children}
); } diff --git a/frontend/app/components/Modal/index.tsx b/frontend/app/components/Modal/index.tsx new file mode 100644 index 000000000..f822178d3 --- /dev/null +++ b/frontend/app/components/Modal/index.tsx @@ -0,0 +1,44 @@ +import React, { Component, createContext } from 'react'; +import Modal from './Modal'; + +const ModalContext = createContext({ + component: null, + props: {}, + showModal: (component: any, props: any) => {}, + hideModal: () => {} +}); + +export class ModalProvider extends Component { + showModal = (component, props = {}) => { + this.setState({ + component, + props + }); + }; + + hideModal = () => + this.setState({ + component: null, + props: {} + }); + + state = { + component: null, + props: {}, + showModal: this.showModal, + hideModal: this.hideModal + }; + + render() { + return ( + + + {this.props.children} + + ); + } +} + +export const ModalConsumer = ModalContext.Consumer; + +export const useModal = () => React.useContext(ModalContext); \ No newline at end of file diff --git a/frontend/app/initialize.js b/frontend/app/initialize.js index 629ea33ad..df06dbbbe 100644 --- a/frontend/app/initialize.js +++ b/frontend/app/initialize.js @@ -5,28 +5,27 @@ 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'; -import { ModalProvider } from './components/Modal/ModalContext'; +import { StoreProvider, RootStore } from './mstore'; +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', () => { - const dashboardStore = new DashboardStore(); render( ( - - + + - - + + - - + + ), document.getElementById('app'), diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx new file mode 100644 index 000000000..ce928b399 --- /dev/null +++ b/frontend/app/mstore/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import DashboardStore, { IDashboardSotre } from 'App/components/Dashboard/store/DashboardStore'; + +export class RootStore { + dashboardStore: IDashboardSotre; + constructor() { + this.dashboardStore = new DashboardStore(); + } +} + +const StoreContext = React.createContext({} as RootStore); + +export const StoreProvider = ({ children, store }) => { + return ( + {children} + ); +}; + +export const useStore = () => React.useContext(StoreContext); + +export const withStore = (Component) => (props) => { + return ; +}; + diff --git a/frontend/app/services/DashboardService.ts b/frontend/app/services/DashboardService.ts new file mode 100644 index 000000000..871fabb3f --- /dev/null +++ b/frontend/app/services/DashboardService.ts @@ -0,0 +1,142 @@ +import { IDashboard } from "App/components/Dashboard/store/dashboard"; +import APIClient from 'App/api_client'; +import { IWidget } from "App/components/Dashboard/store/widget"; + +export interface IDashboardService { + initClient(): void + getWidgets(dashboardId: string): Promise + + getDashboards(): Promise + getDashboard(dashboardId: string): Promise + + saveDashboard(dashboard: IDashboard): Promise + deleteDashboard(dashboardId: string): Promise + + saveMetric(metric: IWidget, dashboardId?: string): Promise + deleteMetric(metricId: string): Promise + + saveWidget(dashboardId: string, widget: IWidget): Promise + deleteWidget(dashboardId: string, widgetId: string): Promise +} + + +export class DashboardService implements IDashboardService { + private client: APIClient; + + constructor(client?: APIClient) { + this.client = client ? client : new APIClient(); + } + + initClient() { + this.client = new APIClient(); + } + + /** + * Get all widgets from a dashboard. + * @param dashboardId Required + * @returns + */ + getWidgets(dashboardId: string): Promise { + return this.client.get(`/dashboards/${dashboardId}/widgets`) + .then(response => response.json()) + .then(response => response.data || []); + } + + + /** + * Get all dashboards. + * @returns {Promise} + */ + getDashboards(): Promise { + return this.client.get('/dashboards') + .then(response => response.json()) + .then(response => response.data || []); + } + + /** + * Get a dashboard by dashboardId. + * @param dashboardId + * @returns {Promise} + */ + getDashboard(dashboardId: string): Promise { + return this.client.get('/dashboards/' + dashboardId) + .then(response => response.json()) + .then(response => response.data || {}); + } + + /** + * Create or update a dashboard. + * @param dashboard Required + * @returns {Promise} + */ + saveDashboard(dashboard: IDashboard): Promise { + const data = dashboard.toJson(); + if (dashboard.dashboardId) { + return this.client.put(`/dashboards/${dashboard.dashboardId}`, data) + .then(response => response.json()) + .then(response => response.data || {}); + } else { + return this.client.post('/dashboards', data) + .then(response => response.json()) + .then(response => response.data || {}); + } + } + + /** + * Delete a dashboard. + * @param dashboardId + * @returns {Promise} + */ + deleteDashboard(dashboardId: string): Promise { + return this.client.delete(`/dashboards/${dashboardId}`) + } + + + /** + * Create a new Meitrc, if the dashboardId is not provided, + * it will add the metric to the dashboard. + * @param metric Required + * @param dashboardId Optional + * @returns {Promise} + */ + 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'; + if (metric.widgetId) { + return this.client.put(path + '/' + metric.widgetId, data) + } else { + return this.client.post(path, data) + } + } + + /** + * Delete a Metric by metricId. + * @param metricId + * @returns {Promise} + */ + deleteMetric(metricId: string): Promise { + return this.client.delete(`/metrics/${metricId}`) + } + + /** + * Remove a widget from a dashboard. + * @param dashboardId Required + * @param widgetId Required + * @returns {Promise} + */ + deleteWidget(dashboardId: string, widgetId: string): Promise { + return this.client.delete(`/dashboards/${dashboardId}/widgets/${widgetId}`) + } + + /** + * Add a widget to a dashboard. + * @param dashboardId Required + * @param widget Required + * @returns {Promise} + */ + saveWidget(dashboardId: string, widget: IWidget): Promise { + return this.client.post(`/dashboards/${dashboardId}/widgets`, widget.toJson()) + } +} \ No newline at end of file diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts new file mode 100644 index 000000000..49ad0eb0c --- /dev/null +++ b/frontend/app/services/index.ts @@ -0,0 +1,3 @@ +import { DashboardService, IDashboardService } from "./DashboardService"; + +export const dashboardService: IDashboardService = new DashboardService();