feat(ui) - dashboard - wip

This commit is contained in:
Shekar Siri 2022-04-05 17:00:29 +02:00
parent 1549c554a7
commit f4f543ec60
32 changed files with 471 additions and 903 deletions

View file

@ -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 {
<Notification />
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
<ModalProvider>
<ModalRoot />
<Switch key="content" >
<Route path={ CLIENT_PATH } component={ Client } />
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
@ -225,6 +229,7 @@ class Router extends React.Component {
)) }
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } />
</Switch>
</ModalProvider>
</Suspense>
</Loader>
:

View file

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

View file

@ -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 (
<div className="bg-white h-screen" style={{ width: '300px'}}>
<div className="color-gray-medium uppercase p-4 text-lg">Dashboards</div>
<div>
{dashboards.map((item: any) => (
<div key={ item.dashboardId } className="px-4">
<SideMenuitem
key={ item.dashboardId }
active={item.dashboardId === activeDashboardId}
title={ item.name }
iconName={ item.icon }
// onClick={() => onItemClick(item)} // TODO add click handler
leading = {(
<div className="ml-2 flex items-center">
{item.isPublic && <div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>}
{item.isPinned && <div className="p-1"><Icon name="pin-fill" size="16" /></div>}
</div>
)}
/>
<SideMenuitem
key={ item.dashboardId }
active={item.dashboardId === activeDashboardId}
title={ item.name }
iconName={ item.icon }
onClick={() => onItemClick(item)} // TODO add click handler
leading = {(
<div className="ml-2 flex items-center">
{item.isPublic && <div className="p-1"><Icon name="user-friends" color="gray-light" size="16" /></div>}
{item.isPinned && <div className="p-1"><Icon name="pin-fill" size="16" /></div>}
</div>
)}
/>
</div>
))}
</div>
@ -32,4 +47,4 @@ function DashbaordListModal(props) {
);
}
export default DashbaordListModal;
export default withRouter(DashbaordListModal);

View file

@ -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 (
<div
className={cn("rounded p-4 shadow border cursor-pointer", { 'bg-active-blue border-color-teal':isSelected, 'bg-white': !isSelected })}
onClick={() => onClick(category)}
>
<div className="font-medium text-lg mb-2">{category.name}</div>
<div className="font-medium text-lg mb-2 capitalize">{category.name}</div>
<div className="mb-2">{category.description}</div>
{selectedCategoryWidgetsCount > 0 && (
<div className="flex items-center">
@ -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<any>(widgetCategories[0]);
const [selectedWidgets, setSelectedWidgets] = React.useState<any>([]);
const selectedWidgetIds = selectedWidgets.map((widget: any) => widget.widgetId);
let widgetCategories: any[] = useObserver(() => dashboardStore.widgetCategories);
const [activeCategory, setActiveCategory] = React.useState<any>();
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 && (
<>
<div className="flex items-baseline">
<h2 className="text-2xl">{activeCategory.name}</h2>
<h2 className="text-2xl capitalize">{activeCategory.name}</h2>
<span className="text-2xl color-gray-medium ml-2">{activeCategory.widgets.length}</span>
</div>
@ -92,14 +74,14 @@ function DashboardMetricSelection(props) {
<div className="grid grid-cols-12 gap-4">
<div className="col-span-3">
<div className="grid grid-cols-1 gap-4">
{widgetCategories.map((category, index) =>
{activeCategory && widgetCategories.map((category, index) =>
<WidgetCategoryItem
key={category.categoryId}
key={category.name}
onClick={handleWidgetCategoryClick}
category={category}
isSelected={activeCategory.categoryId === category.categoryId}
isSelected={activeCategory.name === category.name}
selectedWidgetIds={selectedWidgetIds}
unSelectCategory={removeSelectedWidgetByCategory}
unSelectCategory={dashboardStore.removeSelectedWidgetByCategory}
/>
)}
</div>
@ -108,9 +90,9 @@ function DashboardMetricSelection(props) {
<div className="grid grid-cols-2 gap-4 -mx-4 px-4 lg:grid-cols-2 sm:grid-cols-1">
{activeCategory && activeCategory.widgets.map((widget: any) => (
<div
key={widget.widgetId}
className={cn("border rounded cursor-pointer", { 'border-color-teal' : selectedWidgetIds.includes(widget.widgetId) })}
onClick={() => toggleWidgetSelection(widget)}
key={widget.metricId}
className={cn("border rounded cursor-pointer", { 'border-color-teal' : selectedWidgetIds.includes(widget.metricId) })}
onClick={() => dashboardStore.toggleWidgetSelection(widget)}
>
<WidgetWrapper widget={widget} />
</div>

View file

@ -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%'}}
>
<div className="mb-6">
<h1 className="text-2xl">Create Dashboard</h1>
<div className="mb-6 flex items-end justify-between">
<div>
<h1 className="text-2xl">
{ dashboard.exists() ? "Add metric(s) to dashboard" : "Create Dashboard" }
</h1>
</div>
<div>
<Button outline size="small" onClick={handleCreateNew}>Create New</Button>
</div>
</div>
<DashboardForm />
<p>Create new dashboard by choosing from the range of predefined metrics that you care about. You can always add your custom metrics later.</p>
{ !dashboard.exists() && (
<>
<DashboardForm />
<p>Create new dashboard by choosing from the range of predefined metrics that you care about. You can always add your custom metrics later.</p>
</>
)}
<DashboardMetricSelection />
<div className="flex absolute bottom-0 left-0 right-0 bg-white border-t p-3">
<Button
primary
className=""
disabled={!dashbaord.isValid || loading}
disabled={!dashboard.isValid || loading}
onClick={onSave}
>
Add Selected to Dashboard
{ dashboard.exists() ? "Add Selected to Dashboard" : "Create and Add to Dashboard" }
</Button>
</div>
</div>

View file

@ -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) {
<WidgetView siteId={siteId} {...props} />
</Route>
<Route exact strict path={withSiteId(dashboard(''), siteId)}>
<>Nothing...</>
</Route>
<Route exact strict path={withSiteId(dashboardSelected(dashboardId), siteId)}>
<DashboardView siteId={siteId} />
<DashboardView siteId={siteId} dashboardId={dashboardId} />
</Route>
</Switch>
</div>

View file

@ -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 && (
<div
className="my-2 py-2 color-teal cursor-pointer"
onClick={() => showModal(<DashbaordListModal />, {})}
onClick={() => showModal(<DashbaordListModal siteId={siteId} />, {})}
>
{remainingDashboardsCount} More
</div>

View file

@ -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(<DashboardModal />, {})
}
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 (
<div>
<div className="flex items-center mb-4 justify-between">
<div className="flex items-center">
<PageTitle title={dashboard.name} className="mr-3" />
<Link to={withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId)}><Button primary size="small">Add Metric</Button></Link>
</div>
<Loader loading={loading}>
<NoContent
show={!dashboard || !dashboard.dashboardId}
title="No data available."
size="small"
>
<div>
Right
<div className="flex items-center mb-4 justify-between">
<div className="flex items-center">
<PageTitle title={dashboard?.name} className="mr-3" />
{/* <Link to={withSiteId(dashboardMetricCreate(dashboard?.dashboardId), siteId)}><Button primary size="small">Add Metric</Button></Link> */}
<Button primary size="small" onClick={onEditHandler}>Add Metric</Button>
</div>
<div>
<Button onClick={onDelete}>Remove</Button>
</div>
</div>
<DashboardWidgetGrid />
</div>
</div>
<DashboardWidgetGrid />
</div>
</NoContent>
</Loader>
)
}
export default withModal(observer(DashboardView));
export default withRouter(withModal(observer(DashboardView)));

View file

@ -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) {
<div className="border rounded bg-white">
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
<div className="mr-auto">
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => props.updateSeries(seriesIndex, { name }) } />
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => series.update('name', name) } />
</div>
<div className="flex items-center cursor-pointer">
@ -80,6 +83,7 @@ function FilterSeries(props: Props) {
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
observeChanges={observeChanges}
/>
): (
<div className="color-gray-medium">{emptyMessage}</div>

View file

@ -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) {
<div><Label className="capitalize">{metric.metricType}</Label></div>
<div>
<DashboardLink dashboards={metric.dashboards} />
{/* <div className="link flex items-center">
<div className="mr-2 text-4xl">·</div>
<span>Dashboards</span>
</div> */}
</div>
<div>{metric.owner}</div>
{/* <div className="flex items-center">
<Icon name={metric.isPublic ? "user-friends" : "person-fill"} className="mr-2" />
<span>{metric.isPublic ? 'Team' : 'Private'}</span>
</div> */}
<div>Last Modified</div>
<div>
<div className="flex items-center">
<Icon name={metric.isPublic ? "user-friends" : "person-fill"} className="mr-2" />
<span>{metric.isPublic ? 'Team' : 'Private'}</span>
</div>
</div>
<div>{metric.lastModified && checkForRecent(metric.lastModified, 'LLL dd, yyyy, hh:mm a')}</div>
</div>
);
}

View file

@ -24,7 +24,7 @@ function MetricsList(props: Props) {
<div>Type</div>
<div>Dashboards</div>
<div>Owner</div>
{/* <div>Visibility & Edit Access</div> */}
<div>Visibility & Edit Access</div>
<div>Last Modified</div>
</div>

View file

@ -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<any>({ chart: [{}] })
const [seriesMap, setSeriesMap] = useState<any>([]);
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<any>();
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 <div>Chart</div>;
if (viewType === 'lineChart') {
return (
<CustomMetriLineChart
data={data}
seriesMap={seriesMap}
colors={colors}
params={params}
/>
)
} else if (viewType === 'progress') {
return (
<CustomMetricPercentage
data={data[0]}
colors={colors}
params={params}
/>
)
}
}
if (metricType === 'table') {
return <div>Table</div>;
if (viewType === 'table') {
return <CustomMetricTable metric={metric} data={data[0]} />;
} else if (viewType === 'pieChart') {
return (
<CustomMetricPieChart
metric={metric}
data={data[0]}
colors={colors}
params={params}
/>
)
}
}
return <div>Unknown</div>;
}
return (
<div>
<Loader loading={loading}>
{renderChart()}
</div>
</Loader>
);
}
export default WidgetChart;
export default observer(WidgetChart);

View file

@ -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(() => (
<div className="p-4">
@ -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}
/>
</div>
))}

View file

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

View file

@ -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(() => (
<div
className={cn("rounded bg-white", 'col-span-' + widget.colSpan, { 'border' : !isPreview })}
style={{
@ -69,12 +69,12 @@ function WidgetWrapper(props: Props) {
</div>
</div>
<div className="h-40">
<WidgetChart metric={props.widget} />
<div className="">
<WidgetChart />
</div>
{/* </Link> */}
</div>
);
));
}
export default WidgetWrapper;

View file

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

View file

@ -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<any>
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<IDashboard>
saveMetric(metric: IWidget, dashboardId?: string): Promise<any>
}
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<any> {
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<any> {
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<Dashboard> => {
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),
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,10 +17,10 @@ document.addEventListener('DOMContentLoaded', () => {
<Provider store={ store }>
<StoreProvider store={new RootStore()}>
<DndProvider backend={HTML5Backend}>
<ModalProvider>
<ModalRoot />
{/* <ModalProvider> */}
{/* <ModalRoot /> */}
<Router />
</ModalProvider>
{/* </ModalProvider> */}
</DndProvider>
</StoreProvider>
</Provider>

View file

@ -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<any>
fetch(dashboardId: string)
fetch(dashboardId: string): Promise<any>
save(dashboard: IDashboard): Promise<any>
saveDashboardWidget(dashboard: Dashboard, widget: Widget)
delete(dashboard: IDashboard)
deleteDashboard(dashboard: IDashboard): Promise<any>
toJson(): void
fromJson(json: any): void
initDashboard(dashboard: IDashboard): void
@ -43,14 +50,15 @@ export interface IDashboardSotre {
selectDefaultDashboard(): Promise<IDashboard>
saveMetric(metric: IWidget, dashboardId?: string): Promise<any>
fetchTemplates(): Promise<any>
}
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<any> {
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<any> {
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<any> {
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<any> {
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)
})
}
})
}
}

View file

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

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -10,6 +10,7 @@ export interface IMetricService {
deleteMetric(metricId: string): Promise<any>;
getTemplates(): Promise<any>;
getMetricChartData(metric: IWidget): Promise<any>;
}
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<any> {
const path = metric.metricId ? `/metrics/${metric.metricId}/chart` : `/custom_metrics/try`;
return this.client.get(path)
.then(response => response.json())
.then(response => response.data || {});
}
}