diff --git a/frontend/app/Router.js b/frontend/app/Router.js index a31391127..22b5b3947 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -36,6 +36,8 @@ import { OB_DEFAULT_TAB } from 'App/routes'; import Signup from './components/Signup/Signup'; import { fetchTenants } from 'Duck/user'; import { setSessionPath } from 'Duck/sessions'; +import { ModalProvider } from './components/Modal'; +import ModalRoot from './components/Modal/ModalRoot'; const BugFinder = withSiteIdUpdater(BugFinderPure); const Dashboard = withSiteIdUpdater(DashboardPure); @@ -169,6 +171,8 @@ class Router extends React.Component { }> + + @@ -225,6 +229,7 @@ class Router extends React.Component { )) } + : diff --git a/frontend/app/components/Dashboard/NewDashboard.tsx b/frontend/app/components/Dashboard/NewDashboard.tsx index 56395f8a9..f66dbe4de 100644 --- a/frontend/app/components/Dashboard/NewDashboard.tsx +++ b/frontend/app/components/Dashboard/NewDashboard.tsx @@ -18,12 +18,12 @@ function NewDashboard(props) { useEffect(() => { dashboardStore.fetchList().then((resp) => { - if (dashboardId) { + if (parseInt(dashboardId) > 0) { dashboardStore.selectDashboardById(dashboardId); } else { - dashboardStore.selectDefaultDashboard().then((b) => { + dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => { if (!history.location.pathname.includes('/metrics')) { - history.push(withSiteId(dashboardSelected(b.dashboardId), siteId)); + history.push(withSiteId(dashboardSelected(dashboardId), siteId)); } }); } diff --git a/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx b/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx index 8a5892cb3..d91d058b0 100644 --- a/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx +++ b/frontend/app/components/Dashboard/components/DashbaordListModal/DashbaordListModal.tsx @@ -1,30 +1,45 @@ import React from 'react'; import { useStore } from 'App/mstore'; import { SideMenuitem, SideMenuHeader, Icon, Button } from 'UI'; +import { withSiteId, dashboardSelected, metrics } from 'App/routes'; +import { withRouter } from 'react-router-dom'; +import { useModal } from 'App/components/Modal'; -function DashbaordListModal(props) { +interface Props { + siteId: string + history: any +} +function DashbaordListModal(props: Props) { const { dashboardStore } = useStore(); + const { hideModal } = useModal(); const dashboards = dashboardStore.dashboards; const activeDashboardId = dashboardStore.selectedDashboard?.dashboardId; + + const onItemClick = (dashboard) => { + dashboardStore.selectDashboardById(dashboard.dashboardId); + const path = withSiteId(dashboardSelected(dashboard.dashboardId), parseInt(props.siteId)); + props.history.push(path); + hideModal(); + }; return (
Dashboards
{dashboards.map((item: any) => (
- onItemClick(item)} // TODO add click handler - leading = {( -
- {item.isPublic &&
} - {item.isPinned &&
} -
- )} - /> + onItemClick(item)} // TODO add click handler + leading = {( +
+ {item.isPublic &&
} + {item.isPinned &&
} +
+ )} + />
))}
@@ -32,4 +47,4 @@ function DashbaordListModal(props) { ); } -export default DashbaordListModal; \ No newline at end of file +export default withRouter(DashbaordListModal); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx index 90729935f..78f50a45e 100644 --- a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx +++ b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import WidgetWrapper from '../WidgetWrapper'; import { useObserver } from 'mobx-react-lite'; import cn from 'classnames'; @@ -6,14 +6,14 @@ import { useStore } from 'App/mstore'; function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, unSelectCategory }) { const selectedCategoryWidgetsCount = useObserver(() => { - return category.widgets.filter(widget => selectedWidgetIds.includes(widget.widgetId)).length; + return category.widgets.filter(widget => selectedWidgetIds.includes(widget.metricId)).length; }); return (
onClick(category)} > -
{category.name}
+
{category.name}
{category.description}
{selectedCategoryWidgetsCount > 0 && (
@@ -27,40 +27,22 @@ function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds, function DashboardMetricSelection(props) { const { dashboardStore } = useStore(); - const widgetCategories = dashboardStore?.widgetCategories; - const [activeCategory, setActiveCategory] = React.useState(widgetCategories[0]); - const [selectedWidgets, setSelectedWidgets] = React.useState([]); - const selectedWidgetIds = selectedWidgets.map((widget: any) => widget.widgetId); + let widgetCategories: any[] = useObserver(() => dashboardStore.widgetCategories); + const [activeCategory, setActiveCategory] = React.useState(); + const selectedWidgetIds = useObserver(() => dashboardStore.selectedWidgets.map((widget: any) => widget.metricId)); - const removeSelectedWidgetByCategory = (category: any) => { - const categoryWidgetIds = category.widgets.map((widget: any) => widget.widgetId); - const newSelectedWidgets = selectedWidgets.filter((widget: any) => !categoryWidgetIds.includes(widget.widgetId)); - setSelectedWidgets(newSelectedWidgets); - }; - - const toggleWidgetSelection = (widget: any) => { - console.log('toggleWidgetSelection', widget.widgetId); - if (selectedWidgetIds.includes(widget.widgetId)) { - setSelectedWidgets(selectedWidgets.filter((w: any) => w.widgetId !== widget.widgetId)); - } else { - setSelectedWidgets(selectedWidgets.concat(widget)); - } - }; + useEffect(() => { + dashboardStore?.fetchTemplates().then(templates => { + setActiveCategory(dashboardStore.widgetCategories[0]); + }); + }, []); const handleWidgetCategoryClick = (category: any) => { setActiveCategory(category); }; const toggleAllWidgets = ({ target: { checked }}) => { - if (checked == true) { - const allWidgets = widgetCategories.reduce((acc, category) => { - return acc.concat(category.widgets); - }, []); - - setSelectedWidgets(allWidgets); - } else { - setSelectedWidgets([]); - } + dashboardStore.toggleAllSelectedWidgets(checked); } return useObserver(() => ( @@ -74,7 +56,7 @@ function DashboardMetricSelection(props) { {activeCategory && ( <>
-

{activeCategory.name}

+

{activeCategory.name}

{activeCategory.widgets.length}
@@ -92,14 +74,14 @@ function DashboardMetricSelection(props) {
- {widgetCategories.map((category, index) => + {activeCategory && widgetCategories.map((category, index) => )}
@@ -108,9 +90,9 @@ function DashboardMetricSelection(props) {
{activeCategory && activeCategory.widgets.map((widget: any) => (
toggleWidgetSelection(widget)} + key={widget.metricId} + className={cn("border rounded cursor-pointer", { 'border-color-teal' : selectedWidgetIds.includes(widget.metricId) })} + onClick={() => dashboardStore.toggleWidgetSelection(widget)} >
diff --git a/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx b/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx index 4e3a014c8..8c102763b 100644 --- a/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx +++ b/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx @@ -10,11 +10,15 @@ import { useModal } from 'App/components/Modal'; function DashboardModal(props) { const { dashboardStore } = useStore(); const { hideModal } = useModal(); - const dashbaord = useObserver(() => dashboardStore.dashboardInstance); + const dashboard = useObserver(() => dashboardStore.dashboardInstance); const loading = useObserver(() => dashboardStore.isSaving); const onSave = () => { - dashboardStore.save(dashbaord).then(hideModal) + dashboardStore.save(dashboard).then(hideModal) + } + + const handleCreateNew = () => { + hideModal(); } return useObserver(() => ( @@ -22,21 +26,32 @@ function DashboardModal(props) { className="fixed border-r shadow p-4 h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: '9999', width: '80%'}} > -
-

Create Dashboard

+
+
+

+ { dashboard.exists() ? "Add metric(s) to dashboard" : "Create Dashboard" } +

+
+
+ +
- -

Create new dashboard by choosing from the range of predefined metrics that you care about. You can always add your custom metrics later.

+ { !dashboard.exists() && ( + <> + +

Create new dashboard by choosing from the range of predefined metrics that you care about. You can always add your custom metrics later.

+ + )}
diff --git a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx index 82ab00a0e..171a36164 100644 --- a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx +++ b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx @@ -8,6 +8,7 @@ import { dashboardSelected, dashboardMetricCreate, withSiteId, + dashboard, } from 'App/routes'; import DashboardView from '../DashboardView'; import MetricsView from '../MetricsView'; @@ -34,8 +35,12 @@ function DashboardRouter(props: Props) { + + <>Nothing... + + - +
diff --git a/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx b/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx index c53a7a378..087ae7e07 100644 --- a/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx +++ b/frontend/app/components/Dashboard/components/DashboardSideMenu/DashboardSideMenu.tsx @@ -18,7 +18,7 @@ function DashboardSideMenu(props: Props) { const { hideModal, showModal } = useModal(); const { dashboardStore } = useStore(); const dashboardId = dashboardStore.selectedDashboard?.dashboardId; - const dashboardsPicked = dashboardStore.dashboards.slice(0, SHOW_COUNT); + const dashboardsPicked = useObserver(() => dashboardStore.dashboards.slice(0, SHOW_COUNT)); const remainingDashboardsCount = dashboardStore.dashboards.length - SHOW_COUNT; const redirect = (path) => { @@ -58,7 +58,7 @@ function DashboardSideMenu(props: Props) { {remainingDashboardsCount > 0 && (
showModal(, {})} + onClick={() => showModal(, {})} > {remainingDashboardsCount} More
diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index 4ac3380c6..3c0f96a92 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -1,33 +1,74 @@ -import React from 'react'; -import { observer } from 'mobx-react-lite'; +import React, { useEffect } from 'react'; +import { observer, useObserver } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; -import { Button, PageTitle, Link } from 'UI'; -import { withSiteId, dashboardMetricCreate } from 'App/routes'; +import { Button, PageTitle, Link, Loader, NoContent } from 'UI'; +import { withSiteId, dashboardMetricCreate, dashboardSelected, dashboard } from 'App/routes'; import withModal from 'App/components/Modal/withModal'; import DashboardWidgetGrid from '../DashboardWidgetGrid'; +import { confirm } from 'UI/Confirmation'; +import { withRouter } from 'react-router-dom'; +import { useModal } from 'App/components/Modal'; +import DashboardModal from '../DashboardModal'; interface Props { siteId: number; + history: any + match: any + dashboardId: any } function DashboardView(props: Props) { - const { siteId } = props; + const { siteId, dashboardId } = props; const { dashboardStore } = useStore(); + const { hideModal, showModal } = useModal(); + const loading = useObserver(() => dashboardStore.fetchingDashboard); const dashboard: any = dashboardStore.selectedDashboard + + useEffect(() => { + dashboardStore.fetch(dashboardId) + }, []); + + const onEditHandler = () => { + dashboardStore.initDashboard(dashboard) + showModal(, {}) + } + + const onDelete = async () => { + if (await confirm({ + header: 'Confirm', + confirmButton: 'Yes, Delete', + confirmation: `Are you sure you want to permanently delete this Dashboard?` + })) { + dashboardStore.deleteDashboard(dashboard).then(() => { + dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => { + props.history.push(withSiteId(dashboard(), siteId)); + }); + }); + } + } return ( -
-
-
- - -
+ +
- Right +
+
+ + {/* */} + +
+
+ +
+
+
-
- -
+ + ) } -export default withModal(observer(DashboardView)); \ No newline at end of file +export default withRouter(withModal(observer(DashboardView))); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx index c43e1b97a..337aa1a8a 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import FilterList from 'Shared/Filters/FilterList'; import { edit, @@ -29,13 +29,16 @@ interface Props { removeSeriesFilterFilter: typeof removeSeriesFilterFilter; hideHeader?: boolean; emptyMessage?: any; + observeChanges?: () => void; } function FilterSeries(props: Props) { - const { canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.' } = props; + const { observeChanges = () => {}, canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.' } = props; const [expanded, setExpanded] = useState(true) const { series, seriesIndex } = props; + useEffect(observeChanges, [series]) + const onAddFilter = (filter) => { series.filter.addFilter(filter) } @@ -58,7 +61,7 @@ function FilterSeries(props: Props) {
- props.updateSeries(seriesIndex, { name }) } /> + series.update('name', name) } />
@@ -80,6 +83,7 @@ function FilterSeries(props: Props) { onUpdateFilter={onUpdateFilter} onRemoveFilter={onRemoveFilter} onChangeEventsOrder={onChangeEventsOrder} + observeChanges={observeChanges} /> ): (
{emptyMessage}
diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx index cafcf53d8..b8e623370 100644 --- a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Icon, NoContent, Label, Link, Pagination } from 'UI'; +import { checkForRecent, formatDateTimeDefault, convertTimestampToUtcTimestamp } from 'App/date'; interface Props { metric: any; @@ -30,17 +31,15 @@ function MetricListItem(props: Props) {
- {/*
-
ยท
- Dashboards -
*/}
{metric.owner}
- {/*
- - {metric.isPublic ? 'Team' : 'Private'} -
*/} -
Last Modified
+
+
+ + {metric.isPublic ? 'Team' : 'Private'} +
+
+
{metric.lastModified && checkForRecent(metric.lastModified, 'LLL dd, yyyy, hh:mm a')}
); } diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index 9ef809261..729844590 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -24,7 +24,7 @@ function MetricsList(props: Props) {
Type
Dashboards
Owner
- {/*
Visibility & Edit Access
*/} +
Visibility & Edit Access
Last Modified
diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 53f15faf5..2dd27bc9e 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -1,27 +1,110 @@ -import React from 'react'; - +import React, { useState, useRef, useEffect } from 'react'; +import Period, { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period'; +import CustomMetriLineChart from '../../Widgets/CustomMetricsWidgets/CustomMetriLineChart'; +import CustomMetricPercentage from '../../Widgets/CustomMetricsWidgets/CustomMetricPercentage'; +import CustomMetricTable from '../../Widgets/CustomMetricsWidgets/CustomMetricTable'; +import CustomMetricPieChart from '../../Widgets/CustomMetricsWidgets/CustomMetricPieChart'; +import APIClient from 'App/api_client'; +import { Styles } from '../../Widgets/common'; +import { getChartFormatter } from 'Types/dashboard/helper'; +import { observer, useObserver } from 'mobx-react-lite'; +import { Loader } from 'UI'; +import { useStore } from 'App/mstore'; interface Props { - metric: any; + // metric: any; } function WidgetChart(props: Props) { - const { metric } = props; + // const metric = useObserver(() => props.metric); + const { metricStore } = useStore(); + const metric: any = useObserver(() => metricStore.instance); + const series = useObserver(() => metric.series); + const colors = Styles.customMetricColors; + const [loading, setLoading] = useState(false) + const [data, setData] = useState({ chart: [{}] }) + const [seriesMap, setSeriesMap] = useState([]); + const [period, setPeriod] = useState(Period({ rangeName: metric.rangeName, startDate: metric.startDate, endDate: metric.endDate })); + const params = { density: 70 } + const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' } + const prevMetricRef = useRef(); + + useEffect(() => { + // Check for title change + if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) { + prevMetricRef.current = metric; + return + }; + prevMetricRef.current = metric; + setLoading(true); + + // fetch new data for the widget preview + new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toJson() }) + .then(response => response.json()) + .then(({ errors, data }) => { + if (errors) { + console.log('err', errors) + } else { + const namesMap = data + .map(i => Object.keys(i)) + .flat() + .filter(i => i !== 'time' && i !== 'timestamp') + .reduce((unique: any, item: any) => { + if (!unique.includes(item)) { + unique.push(item); + } + return unique; + }, []); + + setSeriesMap(namesMap); + setData(getChartFormatter(period)(data)); + } + }).finally(() => setLoading(false)); + }, [metric.data]); + const renderChart = () => { - const { metricType } = metric; + const { metricType, viewType } = metric; if (metricType === 'timeseries') { - return
Chart
; + if (viewType === 'lineChart') { + return ( + + ) + } else if (viewType === 'progress') { + return ( + + ) + } } if (metricType === 'table') { - return
Table
; + if (viewType === 'table') { + return ; + } else if (viewType === 'pieChart') { + return ( + + ) + } } return
Unknown
; } return ( -
+ {renderChart()} -
+ ); } -export default WidgetChart; \ No newline at end of file +export default observer(WidgetChart); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 373c399c9..05a2ec910 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -17,9 +17,8 @@ interface Props { function WidgetForm(props: Props) { const { history, match: { params: { siteId, dashboardId, metricId } } } = props; - console.log('WidgetForm params', props.match.params); const { metricStore } = useStore(); - const metric: any = metricStore.instance; + const metric: any = useObserver(() => metricStore.instance); const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries'); const tableOptions = metricOf.filter(i => i.type === 'table'); @@ -31,23 +30,23 @@ function WidgetForm(props: Props) { const writeOption = (e, { value, name }) => { metricStore.merge({ [ name ]: value }); - if (name === 'metricValue') { - metricStore.merge({ metricValue: [value] }); - } - - if (name === 'metricOf') { - if (value === FilterKey.ISSUE) { - metricStore.merge({ metricValue: ['all'] }); + if (name === 'metricValue') { + metricStore.merge({ metricValue: [value] }); } - } - - if (name === 'metricType') { - if (value === 'timeseries') { - metricStore.merge({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }); - } else if (value === 'table') { - metricStore.merge({ metricOf: tableOptions[0].value, viewType: 'table' }); + + if (name === 'metricOf') { + if (value === FilterKey.ISSUE) { + metricStore.merge({ metricValue: ['all'] }); + } + } + + if (name === 'metricType') { + if (value === 'timeseries') { + metricStore.merge({ metricOf: timeseriesOptions[0].value, viewType: 'lineChart' }); + } else if (value === 'table') { + metricStore.merge({ metricOf: tableOptions[0].value, viewType: 'table' }); + } } - } }; const onSave = () => { @@ -61,11 +60,13 @@ function WidgetForm(props: Props) { confirmation: `Are you sure you want to permanently delete this metric?` })) { metricStore.delete(metric).then(props.onDelete); - // props.remove(instance.alertId).then(() => { - // toggleForm(null, false); - // }); } } + + const onObserveChanges = () => { + console.log('observe changes'); + // metricStore.fetchMetricChartData(metric); + } return useObserver(() => (
@@ -156,6 +157,7 @@ function WidgetForm(props: Props) { 'Filter data using any event or attribute. Use Add Step button below to do so.' : 'Add user event or filter to define the series by clicking Add Step.' } + observeChanges={onObserveChanges} />
))} diff --git a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx index 11b9e326c..a227fce93 100644 --- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx +++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx @@ -12,7 +12,7 @@ interface Props { function WidgetPreview(props: Props) { const { className = '' } = props; const { metricStore } = useStore(); - const metric: any = metricStore.instance; + const metric: any = useObserver(() => metricStore.instance); const isTimeSeries = metric.metricType === 'timeseries'; const isTable = metric.metricType === 'table'; diff --git a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx index 92c3f42fd..6a43577a2 100644 --- a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx +++ b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx @@ -1,8 +1,9 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import cn from 'classnames'; import { ItemMenu } from 'UI'; import { useDrag, useDrop } from 'react-dnd'; import WidgetChart from '../WidgetChart'; +import { useObserver } from 'mobx-react-lite'; interface Props { className?: string; @@ -12,7 +13,7 @@ interface Props { isPreview?: boolean; } function WidgetWrapper(props: Props) { - const { widget = {}, index = 0, moveListItem = null, isPreview = false } = props; + const { widget, index = 0, moveListItem = null, isPreview = false } = props; const [{ opacity, isDragging }, dragRef] = useDrag({ type: 'item', @@ -21,7 +22,6 @@ function WidgetWrapper(props: Props) { isDragging: monitor.isDragging(), opacity: monitor.isDragging() ? 0.5 : 1, }), - }); const [{ isOver, canDrop }, dropRef] = useDrop({ @@ -40,7 +40,7 @@ function WidgetWrapper(props: Props) { const ref: any = useRef(null) const dragDropRef: any = dragRef(dropRef(ref)) - return ( + return useObserver(() => (
-
- +
+
{/* */}
- ); + )); } export default WidgetWrapper; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/store/dashboard.ts b/frontend/app/components/Dashboard/store/dashboard.ts deleted file mode 100644 index dd253a6d3..000000000 --- a/frontend/app/components/Dashboard/store/dashboard.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { makeAutoObservable, observable, action, runInAction } from "mobx" -import Widget, { IWidget } from "./widget" -import { dashboardService } from 'App/services' - -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: IWidget[] = [] - isValid: boolean = false - isPinned: boolean = false - currentWidget: IWidget = new Widget() - - constructor() { - makeAutoObservable(this, { - name: observable, - isPublic: observable, - widgets: observable, - isValid: observable, - - toJson: action, - fromJson: action, - addWidget: action, - removeWidget: action, - updateWidget: action, - getWidget: action, - getWidgetIndex: action, - getWidgetByIndex: action, - getWidgetCount: action, - getWidgetIndexByWidgetId: action, - validate: action, - sortWidgets: action, - swapWidgetPosition: action, - update: action, - }) - - this.validate(); - } - - update(data: any) { - runInAction(() => { - Object.assign(this, data) - }) - this.validate() - } - - toJson() { - return { - dashboardId: this.dashboardId, - name: this.name, - isPrivate: this.isPublic, - widgets: this.widgets.map(w => w.toJson()) - } - } - - fromJson(json: any) { - runInAction(() => { - this.dashboardId = json.dashboardId - this.name = json.name - this.isPublic = json.isPublic - this.isPinned = json.isPinned - this.widgets = json.widgets ? json.widgets.map(w => new Widget().fromJson(w)) : [] - }) - return this - } - - validate() { - return this.isValid = this.name.length > 0 - } - - addWidget(widget: IWidget) { - this.widgets.push(widget) - } - - removeWidget(widgetId: string) { - this.widgets = this.widgets.filter(w => w.widgetId !== widgetId) - } - - updateWidget(widget: IWidget) { - const index = this.widgets.findIndex(w => w.widgetId === widget.widgetId) - if (index >= 0) { - this.widgets[index] = widget - } - } - - getWidget(widgetId: string) { - return this.widgets.find(w => w.widgetId === widgetId) - } - - getWidgetIndex(widgetId: string) { - return this.widgets.findIndex(w => w.widgetId === widgetId) - } - - getWidgetByIndex(index: number) { - return this.widgets[index] - } - - getWidgetCount() { - return this.widgets.length - } - - getWidgetIndexByWidgetId(widgetId: string) { - return this.widgets.findIndex(w => w.widgetId === widgetId) - } - - swapWidgetPosition(positionA, positionB) { - console.log('swapWidgetPosition', positionA, positionB) - const widgetA = this.widgets[positionA] - const widgetB = this.widgets[positionB] - this.widgets[positionA] = widgetB - this.widgets[positionB] = widgetA - - widgetA.position = positionB - widgetB.position = positionA - } - - sortWidgets() { - this.widgets = this.widgets.sort((a, b) => { - if (a.position > b.position) { - return 1 - } else if (a.position < b.position) { - return -1 - } else { - return 0 - } - }) - } -} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/store/dashboardStore.ts b/frontend/app/components/Dashboard/store/dashboardStore.ts deleted file mode 100644 index 65dbf861b..000000000 --- a/frontend/app/components/Dashboard/store/dashboardStore.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx" -import Dashboard, { IDashboard } from "./dashboard" -import APIClient from 'App/api_client'; -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() - 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() - - constructor() { - makeAutoObservable(this, { - resetCurrentWidget: action, - addDashboard: action, - removeDashboard: action, - updateDashboard: action, - getDashboard: action, - getDashboardIndex: action, - getDashboardByIndex: action, - getDashboardCount: action, - getDashboardIndexByDashboardId: action, - selectDashboardById: action, - selectDefaultDashboard: action, - toJson: action, - fromJson: action, - setSiteId: action, - editWidget: action, - updateKey: action, - }) - - - // TODO remove this sample data - // this.dashboards = sampleDashboards - - // for (let i = 0; i < 15; i++) { - // const widget: any= {}; - // widget.widgetId = `${i}` - // widget.name = `Widget ${i}`; - // widget.metricType = ['timeseries', 'table'][Math.floor(Math.random() * 2)]; - // this.widgets.push(widget) - // } - - for (let i = 0; i < 4; i++) { - const cat: any = { - name: `Category ${i + 1}`, - categoryId: i, - description: `Category ${i + 1} description`, - widgets: [] - } - - const randomNumber = Math.floor(Math.random() * (5 - 2 + 1)) + 2 - for (let j = 0; j < randomNumber; j++) { - const widget: any= {}; - widget.widgetId = `${i}-${j}` - widget.name = `Widget ${i}-${j}`; - widget.metricType = ['timeseries', 'table'][Math.floor(Math.random() * 2)]; - cat.widgets.push(widget); - } - - this.widgetCategories.push(cat) - } - } - - initDashboard(dashboard: Dashboard) { - this.dashboardInstance = dashboard || new Dashboard() - } - - updateKey(key: any, value: any) { - this[key] = value - } - - resetCurrentWidget() { - this.currentWidget = new Widget() - } - - editWidget(widget: any) { - this.currentWidget.update(widget) - } - - fetchList() { - this.isLoading = true - - dashboardService.getDashboards() - .then((list: any) => { - runInAction(() => { - this.dashboards = list.map(d => new Dashboard().fromJson(d)) - }) - }).finally(() => { - runInAction(() => { - this.isLoading = false - }) - }) - } - - fetch(dashboardId: string) { - this.isLoading = true - dashboardService.getDashboard(dashboardId).then(response => { - runInAction(() => { - this.selectedDashboard = new Dashboard().fromJson(response) - }) - }).finally(() => { - runInAction(() => { - this.isLoading = false - }) - }) - } - - save(dashboard: IDashboard): Promise { - this.isSaving = true - const isCreating = !dashboard.dashboardId - return dashboardService.saveDashboard(dashboard).then(_dashboard => { - runInAction(() => { - if (isCreating) { - this.addDashboard(_dashboard) - } else { - this.updateDashboard(_dashboard) - } - }) - }).finally(() => { - runInAction(() => { - this.isSaving = false - }) - }) - } - - saveMetric(metric: IWidget, dashboardId: string): Promise { - const isCreating = !metric.widgetId - return dashboardService.saveMetric(metric, dashboardId).then(metric => { - runInAction(() => { - if (isCreating) { - this.selectedDashboard?.widgets.push(metric) - } else { - this.selectedDashboard?.widgets.map(w => { - if (w.widgetId === metric.widgetId) { - w.update(metric) - } - }) - } - }) - }) - } - - saveDashboardWidget(dashboard: Dashboard, widget: Widget) { - widget.validate() - if (widget.isValid) { - this.isLoading = true - 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 - } - - addDashboard(dashboard: Dashboard) { - this.dashboards.push(dashboard) - } - - removeDashboard(dashboard: Dashboard) { - this.dashboards = this.dashboards.filter(d => d !== dashboard) - } - - getDashboard(dashboardId: string) { - return this.dashboards.find(d => d.dashboardId === dashboardId) - } - - getDashboardIndex(dashboardId: string) { - return this.dashboards.findIndex(d => d.dashboardId === dashboardId) - } - - getDashboardByIndex(index: number) { - return this.dashboards[index] - } - - getDashboardCount() { - return this.dashboards.length - } - - getDashboardIndexByDashboardId(dashboardId: string) { - return this.dashboards.findIndex(d => d.dashboardId === dashboardId) - } - - updateDashboard(dashboard: Dashboard) { - const index = this.dashboards.findIndex(d => d.dashboardId === dashboard.dashboardId) - if (index >= 0) { - this.dashboards[index] = dashboard - } - } - - selectDashboardById = (dashboardId: any) => { - this.selectedDashboard = this.dashboards.find(d => d.dashboardId == dashboardId) || new Dashboard(); - if (this.selectedDashboard.dashboardId) { - this.fetch(this.selectedDashboard.dashboardId) - } - } - - setSiteId = (siteId: any) => { - this.siteId = siteId - } - - selectDefaultDashboard = (): Promise => { - return new Promise((resolve, reject) => { - if (this.dashboards.length > 0) { - const pinnedDashboard = this.dashboards.find(d => d.isPinned) - if (pinnedDashboard) { - this.selectedDashboard = pinnedDashboard - } else { - this.selectedDashboard = this.dashboards[0] - } - } - if (this.selectedDashboard) { - resolve(this.selectedDashboard) - } - - reject(new Error("No dashboards found")) - }) - } -} - -function getRandomWidget() { - const widget = new Widget(); - widget.widgetId = Math.floor(Math.random() * 100); - widget.name = randomMetricName(); - // widget.type = "random"; - widget.colSpan = Math.floor(Math.random() * 2) + 1; - return widget; -} - -function generateRandomPlaceName() { - const placeNames = [ - "New York", - "Los Angeles", - "Chicago", - "Houston", - "Philadelphia", - "Phoenix", - "San Antonio", - "San Diego", - ] - return placeNames[Math.floor(Math.random() * placeNames.length)] -} - - -function randomMetricName () { - const metrics = ["Revenue", "Profit", "Expenses", "Sales", "Orders", "Revenue", "Profit", "Expenses", "Sales", "Orders", "Revenue", "Profit", "Expenses", "Sales", "Orders", "Revenue", "Profit", "Expenses", "Sales", "Orders"]; - return metrics[Math.floor(Math.random() * metrics.length)]; -} - -function getRandomDashboard(id: any = null, isPinned = false) { - const dashboard = new Dashboard(); - dashboard.name = generateRandomPlaceName(); - dashboard.dashboardId = id ? id : Math.floor(Math.random() * 10); - dashboard.isPinned = isPinned; - for (let i = 0; i < 8; i++) { - const widget = getRandomWidget(); - widget.position = i; - dashboard.addWidget(widget); - } - return dashboard; -} - -const sampleDashboards = [ - getRandomDashboard(1, true), - getRandomDashboard(2), - getRandomDashboard(3), - getRandomDashboard(4), -] \ No newline at end of file diff --git a/frontend/app/components/Dashboard/store/filter.ts b/frontend/app/components/Dashboard/store/filter.ts deleted file mode 100644 index c9bdd3452..000000000 --- a/frontend/app/components/Dashboard/store/filter.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx" - -export default class Filter { - name: string = '' - filters: any[] = [] - eventsOrder: string = 'then' - - constructor() { - makeAutoObservable(this, { - addFilter: action, - removeFilter: action, - updateKey: action, - }) - } - - addFilter(filter: any) { - filter.value = [""] - if (filter.hasOwnProperty('filters')) { - filter.filters = filter.filters.map(i => ({ ...i, value: [""] })) - } - this.filters.push(filter) - console.log('addFilter', this.filters) - } - - updateFilter(index: number, filter: any) { - this.filters[index] = filter - console.log('updateFilter', this.filters) - } - - updateKey(key, value) { - this[key] = value - } - - removeFilter(index: number) { - this.filters.splice(index, 1) - } -} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/store/filterSeries.ts b/frontend/app/components/Dashboard/store/filterSeries.ts deleted file mode 100644 index 284e50230..000000000 --- a/frontend/app/components/Dashboard/store/filterSeries.ts +++ /dev/null @@ -1,22 +0,0 @@ -// import Filter from 'Types/filter'; -import Filter from './filter' -import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx" - -export default class FilterSeries { - seriesId?: any = undefined - name: string = "Series 1" - filter: Filter = new Filter() - - constructor() { - makeAutoObservable(this, { - name: observable, - filter: observable, - - update: action, - }) - } - - update(key, value) { - this[key] = value - } -} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/store/index.ts b/frontend/app/components/Dashboard/store/index.ts deleted file mode 100644 index 6baa3c043..000000000 --- a/frontend/app/components/Dashboard/store/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as DashboardStore } from './dashboardStore'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/store/widget.ts b/frontend/app/components/Dashboard/store/widget.ts deleted file mode 100644 index 5e624dd54..000000000 --- a/frontend/app/components/Dashboard/store/widget.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx" -import Filter from 'Types/filter'; -import FilterSeries from "./filterSeries"; - -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 { - public static get ID_KEY():string { return "widgetId" } - widgetId: any = undefined - name: string = "New Metric" - metricType: string = "timeseries" - metricOf: string = "sessionCount" - metricValue: string = "" - viewType: string = "lineChart" - series: FilterSeries[] = [] - sessions: [] = [] - isPublic: boolean = false - owner: string = "" - lastModified: Date = new Date() - dashboardIds: any[] = [] - - position: number = 0 - data: any = {} - isLoading: boolean = false - isValid: boolean = false - dashboardId: any = undefined - colSpan: number = 2 - - constructor() { - makeAutoObservable(this, { - widgetId: observable, - name: observable, - metricType: observable, - metricOf: observable, - position: observable, - data: observable, - isLoading: observable, - isValid: observable, - dashboardId: observable, - addSeries: action, - colSpan: observable, - - fromJson: action, - toJson: action, - validate: action, - update: action, - udpateKey: action, - }) - - const filterSeries = new FilterSeries() - this.series.push(filterSeries) - } - - udpateKey(key: string, value: any) { - this[key] = value - } - - removeSeries(index: number) { - this.series.splice(index, 1) - } - - addSeries() { - const series = new FilterSeries() - series.name = "Series " + (this.series.length + 1) - this.series.push(series) - } - - - fromJson(json: any) { - runInAction(() => { - this.widgetId = json.widgetId - this.name = json.name - this.data = json.data - }) - return this - } - - toJson() { - return { - widgetId: this.widgetId, - name: this.name, - metricOf: this.metricOf, - metricValue: this.metricValue, - viewType: this.viewType, - series: this.series, - sessions: this.sessions, - isPublic: this.isPublic, - } - } - - 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/shared/Filters/FilterList/FilterList.tsx b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx index 110702ac7..5aab22bec 100644 --- a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx +++ b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx @@ -1,4 +1,4 @@ -import React, { useState} from 'react'; +import React, { useState, useEffect } from 'react'; import FilterItem from '../FilterItem'; import { SegmentSelection, Popup } from 'UI'; import { List } from 'immutable'; @@ -11,14 +11,17 @@ interface Props { onRemoveFilter: (filterIndex) => void; onChangeEventsOrder: (e, { name, value }) => void; hideEventsOrder?: boolean; + observeChanges?: () => void; } function FilterList(props: Props) { - const { filter, hideEventsOrder = false } = props; + const { observeChanges = () => {}, filter, hideEventsOrder = false } = props; const filters = List(filter.filters); const hasEvents = filters.filter((i: any) => i.isEvent).size > 0; const hasFilters = filters.filter((i: any) => !i.isEvent).size > 0; let rowIndex = 0; + useEffect(observeChanges, [filters]); + const onRemoveFilter = (filterIndex) => { props.onRemoveFilter(filterIndex); } diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index ba7b8650e..2fd2fc132 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -12,7 +12,7 @@ interface Props { } function FilterValue(props: Props) { const { filter } = props; - const [durationValues, setDurationValues] = useState({ minDuration: filter.value[0], maxDuration: filter.value[1] }); + const [durationValues, setDurationValues] = useState({ minDuration: filter.value[0], maxDuration: filter.value.length > 1 ? filter.value[1] : filter.value[0] }); const showCloseButton = filter.value.length > 1; const lastIndex = filter.value.length - 1; diff --git a/frontend/app/initialize.js b/frontend/app/initialize.js index 9e73942f7..dcfd01058 100644 --- a/frontend/app/initialize.js +++ b/frontend/app/initialize.js @@ -17,10 +17,10 @@ document.addEventListener('DOMContentLoaded', () => { - - + {/* */} + {/* */} - + {/* */} diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index 28e7197fb..96d7b22e0 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -1,13 +1,14 @@ import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx" import Dashboard, { IDashboard } from "./types/dashboard" import Widget, { IWidget } from "./types/widget"; -import { dashboardService } from "App/services"; +import { dashboardService, metricService } from "App/services"; export interface IDashboardSotre { dashboards: IDashboard[] - widgetTemplates: any[] selectedDashboard: IDashboard | null dashboardInstance: IDashboard + selectedWidgets: IWidget[] + siteId: any currentWidget: Widget widgetCategories: any[] @@ -18,16 +19,22 @@ export interface IDashboardSotre { isLoading: boolean isSaving: boolean + isDeleting: boolean + fetchingDashboard: boolean + toggleAllSelectedWidgets: (isSelected: boolean) => void + removeSelectedWidgetByCategory(category: string): void + toggleWidgetSelection(widget: IWidget): void + initDashboard(dashboard?: IDashboard): void updateKey(key: string, value: any): void resetCurrentWidget(): void editWidget(widget: any): void fetchList(): Promise - fetch(dashboardId: string) + fetch(dashboardId: string): Promise save(dashboard: IDashboard): Promise saveDashboardWidget(dashboard: Dashboard, widget: Widget) - delete(dashboard: IDashboard) + deleteDashboard(dashboard: IDashboard): Promise toJson(): void fromJson(json: any): void initDashboard(dashboard: IDashboard): void @@ -43,14 +50,15 @@ export interface IDashboardSotre { selectDefaultDashboard(): Promise saveMetric(metric: IWidget, dashboardId?: string): Promise + fetchTemplates(): Promise } export default class DashboardStore implements IDashboardSotre { siteId: any = null // Dashbaord / Widgets dashboards: Dashboard[] = [] - widgetTemplates: any[] = [] - selectedDashboard: Dashboard | null = new Dashboard() + selectedDashboard: Dashboard | null = null dashboardInstance: IDashboard = new Dashboard() + selectedWidgets: IWidget[] = []; currentWidget: Widget = new Widget() widgetCategories: any[] = [] widgets: Widget[] = [] @@ -63,9 +71,13 @@ export default class DashboardStore implements IDashboardSotre { // Loading states isLoading: boolean = true isSaving: boolean = false + isDeleting: boolean = false + fetchingDashboard: boolean = false constructor() { makeAutoObservable(this, { + widgetCategories: observable.ref, + resetCurrentWidget: action, addDashboard: action, removeDashboard: action, @@ -82,41 +94,65 @@ export default class DashboardStore implements IDashboardSotre { setSiteId: action, editWidget: action, updateKey: action, + + toggleAllSelectedWidgets: action, + removeSelectedWidgetByCategory: action, + toggleWidgetSelection: action, + fetchTemplates: action, }) // TODO remove this sample data - // this.dashboards = sampleDashboards - // for (let i = 0; i < 15; i++) { - // const widget: any= {}; - // widget.widgetId = `${i}` - // widget.name = `Widget ${i}`; - // widget.metricType = ['timeseries', 'table'][Math.floor(Math.random() * 2)]; - // this.widgets.push(widget) - // } - - for (let i = 0; i < 4; i++) { - const cat: any = { - name: `Category ${i + 1}`, - categoryId: i, - description: `Category ${i + 1} description`, - widgets: [] - } + // for (let i = 0; i < 4; i++) { + // const cat: any = { + // name: `Category ${i + 1}`, + // categoryId: i, + // description: `Category ${i + 1} description`, + // widgets: [] + // } - const randomNumber = Math.floor(Math.random() * (5 - 2 + 1)) + 2 - for (let j = 0; j < randomNumber; j++) { - const widget: any= {}; - widget.widgetId = `${i}-${j}` - widget.name = `Widget ${i}-${j}`; - widget.metricType = ['timeseries', 'table'][Math.floor(Math.random() * 2)]; - cat.widgets.push(widget); - } + // const randomNumber = Math.floor(Math.random() * (5 - 2 + 1)) + 2 + // for (let j = 0; j < randomNumber; j++) { + // const widget: any= new Widget(); + // widget.widgetId = `${i}-${j}` + // widget.viewType = 'lineChart' + // widget.name = `Widget ${i}-${j}`; + // // widget.metricType = ['timeseries', 'table'][Math.floor(Math.random() * 2)]; + // widget.metricType = 'timeseries'; + // cat.widgets.push(widget); + // } - this.widgetCategories.push(cat) + // this.widgetCategories.push(cat) + // } + } + + toggleAllSelectedWidgets(isSelected: boolean) { + if (isSelected) { + const allWidgets = this.widgetCategories.reduce((acc, cat) => { + return acc.concat(cat.widgets) + }, []) + + this.selectedWidgets = allWidgets + } else { + this.selectedWidgets = [] } } + removeSelectedWidgetByCategory = (category: any) => { + const categoryWidgetIds = category.widgets.map(w => w.metricId) + this.selectedWidgets = this.selectedWidgets.filter((widget: any) => !categoryWidgetIds.includes(widget.metricId)); + } + + toggleWidgetSelection = (widget: any) => { + const selectedWidgetIds = this.selectedWidgets.map((widget: any) => widget.metricId); + if (selectedWidgetIds.includes(widget.metricId)) { + this.selectedWidgets = this.selectedWidgets.filter((w: any) => w.metricId !== widget.metricId); + } else { + this.selectedWidgets.push(widget); + } + }; + findByIds(ids: string[]) { return this.dashboards.filter(d => ids.includes(d.dashboardId)) } @@ -152,22 +188,23 @@ export default class DashboardStore implements IDashboardSotre { }) } - fetch(dashboardId: string) { - this.isLoading = true - dashboardService.getDashboard(dashboardId).then(response => { - runInAction(() => { - this.selectedDashboard = new Dashboard().fromJson(response) - }) + fetch(dashboardId: string): Promise { + this.fetchingDashboard = true + return dashboardService.getDashboard(dashboardId).then(response => { + this.selectedDashboard = new Dashboard().fromJson(response) }).finally(() => { - runInAction(() => { - this.isLoading = false - }) + this.fetchingDashboard = false }) } save(dashboard: IDashboard): Promise { this.isSaving = true const isCreating = !dashboard.dashboardId + + if (isCreating) { + dashboard.metrics = this.selectedWidgets.map(w => w.metricId) + } + return dashboardService.saveDashboard(dashboard).then(_dashboard => { runInAction(() => { if (isCreating) { @@ -207,8 +244,17 @@ export default class DashboardStore implements IDashboardSotre { } } - delete(dashboard: Dashboard) { - this.isLoading = true + deleteDashboard(dashboard: Dashboard): Promise { + this.isDeleting = true + return dashboardService.deleteDashboard(dashboard.dashboardId).then(() => { + runInAction(() => { + this.removeDashboard(dashboard) + }) + }).finally(() => { + runInAction(() => { + this.isDeleting = false + }) + }) } toJson() { @@ -229,7 +275,7 @@ export default class DashboardStore implements IDashboardSotre { } removeDashboard(dashboard: Dashboard) { - this.dashboards = this.dashboards.filter(d => d !== dashboard) + this.dashboards = this.dashboards.filter(d => d.dashboardId !== dashboard.dashboardId) } getDashboard(dashboardId: string) { @@ -275,18 +321,43 @@ export default class DashboardStore implements IDashboardSotre { if (this.dashboards.length > 0) { const pinnedDashboard = this.dashboards.find(d => d.isPinned) if (pinnedDashboard) { - console.log('selecting pined dashboard') this.selectedDashboard = pinnedDashboard } else { - console.log('selecting first dashboard') this.selectedDashboard = this.dashboards[0] } } if (this.selectedDashboard) { resolve(this.selectedDashboard) } + // reject(new Error("No dashboards found")) + }) + } - reject(new Error("No dashboards found")) + fetchTemplates(): Promise { + return new Promise((resolve, reject) => { + if (this.widgetCategories.length > 0) { + resolve(this.widgetCategories) + } else { + metricService.getTemplates().then(response => { + const categories: any[] = [] + response.forEach(category => { + const widgets: any[] = [] + category.widgets.forEach(widget => { + const w = new Widget().fromJson(widget) + widgets.push(w) + }) + const c: any = {} + c.widgets = widgets + c.name = category.category + c.description = category.description + categories.push(c) + }) + this.widgetCategories = categories + resolve(this.widgetCategories) + }).catch(error => { + reject(error) + }) + } }) } } diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index e312ee58c..1e3746b9e 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -31,6 +31,7 @@ export interface IMetricStore { fetchList(): void fetch(metricId: string) delete(metric: IWidget) + // fetchMetricChartData(metric: IWidget) } export default class MetricStore implements IMetricStore { @@ -69,7 +70,6 @@ export default class MetricStore implements IMetricStore { fetch: action, delete: action, - paginatedList: computed, }) @@ -85,7 +85,7 @@ export default class MetricStore implements IMetricStore { // State Actions init(metric?: IWidget|null) { - this.instance = metric || new Widget() + this.instance.update(metric || new Widget()) } updateKey(key: string, value: any) { @@ -149,7 +149,7 @@ export default class MetricStore implements IMetricStore { this.isLoading = true return metricService.getMetrics() .then(metrics => { - this.metrics = metrics + this.metrics = metrics.map(m => new Widget().fromJson(m)) }).finally(() => { this.isLoading = false }) @@ -174,4 +174,17 @@ export default class MetricStore implements IMetricStore { this.isSaving = false }) } + + fetchMetricChartData(metric: IWidget) { + this.isLoading = true + return metricService.getMetricChartData(metric) + .then(data => { + console.log('data', data) + // runInAction(() => { + // metric.data = data + // }) + }).finally(() => { + this.isLoading = false + }) + } } \ No newline at end of file diff --git a/frontend/app/mstore/types/dashboard.ts b/frontend/app/mstore/types/dashboard.ts index 7f3095744..46973f9ff 100644 --- a/frontend/app/mstore/types/dashboard.ts +++ b/frontend/app/mstore/types/dashboard.ts @@ -6,6 +6,7 @@ export interface IDashboard { name: string isPublic: boolean widgets: IWidget[] + metrics: any[] isValid: boolean isPinned: boolean currentWidget: IWidget @@ -24,13 +25,15 @@ export interface IDashboard { getWidgetIndexByWidgetId(widgetId: string): void swapWidgetPosition(positionA: number, positionB: number): void sortWidgets(): void + exists(): boolean } export default class Dashboard implements IDashboard { public static get ID_KEY():string { return "dashboardId" } dashboardId: any = undefined - name: string = "New Dashboard X" + name: string = "New Dashboard" isPublic: boolean = false widgets: IWidget[] = [] + metrics: any[] = [] isValid: boolean = false isPinned: boolean = false currentWidget: IWidget = new Widget() @@ -73,7 +76,9 @@ export default class Dashboard implements IDashboard { dashboardId: this.dashboardId, name: this.name, isPrivate: this.isPublic, - widgets: this.widgets.map(w => w.toJson()) + // widgets: this.widgets.map(w => w.toJson()) + // widgets: this.widgets + metrics: this.metrics } } @@ -149,4 +154,8 @@ export default class Dashboard implements IDashboard { } }) } + + exists() { + return this.dashboardId !== undefined + } } \ No newline at end of file diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts index 5a7dcc5e9..bbe303e78 100644 --- a/frontend/app/mstore/types/filter.ts +++ b/frontend/app/mstore/types/filter.ts @@ -9,11 +9,14 @@ import FilterItem from "./filterItem" export default class Filter { public static get ID_KEY():string { return "filterId" } name: string = '' - filters: any[] = [] + filters: FilterItem[] = [] eventsOrder: string = 'then' constructor() { makeAutoObservable(this, { + filters: observable, + eventsOrder: observable, + addFilter: action, removeFilter: action, updateKey: action, diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index ea56020c5..606767b2b 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -19,6 +19,11 @@ export default class FilterItem { type: observable, key: observable, value: observable, + operator: observable, + source: observable, + filters: observable, + + merge: action }) this.merge(data) diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index c55ce2e97..4e6881b58 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -1,8 +1,9 @@ -import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx" +import { makeAutoObservable, runInAction, observable, action, reaction, computed } from "mobx" import FilterSeries from "./filterSeries"; +import { DateTime } from 'luxon'; export interface IWidget { - metricId: string + metricId: any widgetId: any name: string metricType: string @@ -59,18 +60,20 @@ export default class Widget implements IWidget { constructor() { makeAutoObservable(this, { + // data: observable, widgetId: observable, name: observable, metricType: observable, metricOf: observable, position: observable, - data: observable, isLoading: observable, isValid: observable, dashboardId: observable, - addSeries: action, colSpan: observable, - + series: observable, + + addSeries: action, + removeSeries: action, fromJson: action, toJson: action, validate: action, @@ -97,14 +100,17 @@ export default class Widget implements IWidget { } fromJson(json: any) { - console.log('json', json); runInAction(() => { this.metricId = json.metricId this.widgetId = json.widgetId + this.metricValue = json.metricValue + this.metricOf = json.metricOf + this.metricType = json.metricType this.name = json.name - this.data = json.data - this.series = json.series.map((series: any) => new FilterSeries().fromJson(series)), + this.series = json.series ? json.series.map((series: any) => new FilterSeries().fromJson(series)) : [], this.dashboards = json.dashboards + this.owner = json.ownerEmail + this.lastModified = DateTime.fromISO(json.editedAt || json.createdAt) }) return this } diff --git a/frontend/app/services/DashboardService.ts b/frontend/app/services/DashboardService.ts index 1e5e8974f..a9d49a3bd 100644 --- a/frontend/app/services/DashboardService.ts +++ b/frontend/app/services/DashboardService.ts @@ -1,6 +1,6 @@ -import { IDashboard } from "App/components/Dashboard/store/dashboard"; +import { IDashboard } from "App/mstore/types/dashboard"; import APIClient from 'App/api_client'; -import { IWidget } from "App/components/Dashboard/store/widget"; +import { IWidget } from "App/mstore/types/widget"; export interface IDashboardService { initClient(client?: APIClient) diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts index 99c935d47..f2fab0a83 100644 --- a/frontend/app/services/MetricService.ts +++ b/frontend/app/services/MetricService.ts @@ -10,6 +10,7 @@ export interface IMetricService { deleteMetric(metricId: string): Promise; getTemplates(): Promise; + getMetricChartData(metric: IWidget): Promise; } export default class MetricService implements IMetricService { @@ -88,4 +89,11 @@ export default class MetricService implements IMetricService { .then(response => response.json()) .then(response => response.data || []); } + + getMetricChartData(metric: IWidget): Promise { + const path = metric.metricId ? `/metrics/${metric.metricId}/chart` : `/custom_metrics/try`; + return this.client.get(path) + .then(response => response.json()) + .then(response => response.data || {}); + } } \ No newline at end of file